EAM: Fulfillment updated event (#810)

* Add fulfillment update event

* Add changeset

* Improve comments
This commit is contained in:
Krzysztof Wolski 2023-07-27 10:36:43 +02:00 committed by GitHub
parent 4c7c1c15d3
commit c07ddb33d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 583 additions and 71 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-emails-and-messages": patch
---
Added support for the new event: fulfillment updated.

View file

@ -0,0 +1,310 @@
import { MessageEventTypes } from "../modules/event-handlers/message-event-types";
// Notify webhook event groups multiple event types under the one webhook. We need to map it to events recognized by the App
export const notifyEventMapping: Record<string, MessageEventTypes> = {
account_confirmation: "ACCOUNT_CONFIRMATION",
account_delete: "ACCOUNT_DELETE",
account_password_reset: "ACCOUNT_PASSWORD_RESET",
account_change_email_request: "ACCOUNT_CHANGE_EMAIL_REQUEST",
account_change_email_confirm: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
order_fulfillment_update: "ORDER_FULFILLMENT_UPDATE",
};
interface IssuingPrincipal {
id?: string;
type?: string;
}
interface Meta {
issued_at: string;
version: string;
issuing_principal: IssuingPrincipal;
}
export type NotifySubscriptionPayload = {
meta: Meta;
} & (
| {
notify_event: "account_confirmation";
payload: NotifyPayloadAccountConfirmation;
}
| {
notify_event: "account_delete";
payload: NotifyPayloadAccountDelete;
}
| {
notify_event: "account_password_reset";
payload: NotifyPayloadAccountPasswordReset;
}
| {
notify_event: "account_change_email";
payload: NotifyPayloadAccountChangeEmailRequest;
}
| {
notify_event: "account_change_email_confirm";
payload: NotifyPayloadAccountChangeEmailConfirmation;
}
| {
notify_event: "order_fulfillment_update";
payload: NotifyPayloadFulfillmentUpdate;
}
);
export interface NotifyPayloadAccountConfirmation {
channel_slug: string;
confirm_url: string;
domain: string;
logo_url: string;
recipient_email: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountDelete {
channel_slug: string;
delete_url: string;
domain: string;
logo_url: string;
recipient_email: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountPasswordReset {
channel_slug: string;
domain: string;
logo_url: string;
recipient_email: string;
reset_url: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountChangeEmailRequest {
channel_slug: string;
domain: string;
logo_url: string;
new_email: string;
old_email: string;
recipient_email: string;
reset_url: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountChangeEmailConfirmation {
channel_slug: string;
domain: string;
logo_url: string;
recipient_email: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadFulfillmentUpdate {
channel_slug: string;
digital_lines: DigitalLine[];
domain: string;
fulfillment: Fulfillment;
logo_url: string;
order: Order;
physical_lines: PhysicalLine[];
recipient_email: string;
site_name: string;
token: string;
}
interface User {
email: string;
first_name: string;
id: string;
is_active: boolean;
is_staff: boolean;
language_code: string;
last_name: string;
metadata: Metadata | null;
private_metadata: Metadata | null;
}
type Metadata = Record<string, string>;
interface Order {
billing_address: Address;
channel_slug: string;
collection_point_name: string | null;
created: string;
currency: string;
discount_amount: number;
display_gross_prices: boolean;
email: string;
id: string;
language_code: string;
lines: Line[];
metadata: Metadata | null;
number: number;
order_details_url: string;
private_metadata: Metadata | null;
shipping_address: Address;
shipping_method_name: string;
shipping_price_gross_amount: string;
shipping_price_net_amount: string;
status: string;
subtotal_gross_amount: string;
subtotal_net_amount: string;
tax_amount: string;
token: string;
total_gross_amount: string;
total_net_amount: string;
undiscounted_total_gross_amount: string;
undiscounted_total_net_amount: string;
voucher_discount: number | null;
}
interface Line {
currency: string;
digital_url: string | null;
id: string;
is_digital: boolean;
is_shipping_required: boolean;
metadata: Metadata | null;
product_name: string;
product_sku: string;
product_variant_id: string;
product: Product;
quantity_fulfilled: number;
quantity: number;
tax_rate: string;
total_gross_amount: string;
total_net_amount: string;
total_tax_amount: string;
translated_product_name: string;
translated_variant_name: string;
unit_discount_amount: string;
unit_discount_reason: string | null;
unit_discount_type: string;
unit_discount_value: string;
unit_price_gross_amount: string;
unit_price_net_amount: string;
unit_tax_amount: string;
variant_name: string;
variant: Variant;
}
interface Product {
attributes: AttributeWithAssignment[];
first_image: Image;
id: string;
images: Image[];
weight: string;
}
interface Variant {
first_image: Image;
id: string;
images: Image[];
is_preorder: boolean;
preorder_end_date: string | null;
weight: string;
}
interface AttributeWithAssignment {
assignment: AttributeAssignment;
values: AttributeValue[];
}
interface AttributeAssignment {
attribute: Attribute;
}
interface Attribute {
name: string;
slug: string;
}
interface AttributeValue {
file_url: string | null;
name: string;
slug: string | null;
value: string;
}
interface ImageSizeMapping {
"32": string;
"64": string;
"128": string;
"256": string;
"512": string;
"1024": string;
"2048": string;
"4096": string;
}
interface Image {
original: ImageSizeMapping;
}
interface Address {
first_name: string;
last_name: string;
company_name: string;
street_address_1: string;
street_address_2: string;
city: string;
city_area: string;
postal_code: string;
country: string;
country_area: string;
phone: string;
}
interface Fulfillment {
tracking_number: string;
is_tracking_number_url: boolean;
}
interface PhysicalLine {
id: string;
order_line: OrderLine;
quantity: number;
}
interface DigitalLine {
id: string;
order_line: OrderLine;
quantity: number;
}
interface OrderLine {
currency: string;
digital_url: string | null;
id: string;
is_digital: boolean;
is_shipping_required: boolean;
metadata: Metadata | null;
product_name: string;
product_sku: string;
product_variant_id: string;
product: Product;
quantity_fulfilled: number;
quantity: number;
tax_rate: string;
total_gross_amount: string;
total_net_amount: string;
total_tax_amount: string;
translated_product_name: string;
translated_variant_name: string;
unit_discount_amount: string;
unit_discount_reason: string | null;
unit_discount_type: string;
unit_discount_value: string;
unit_price_gross_amount: string;
unit_price_net_amount: string;
unit_tax_amount: string;
variant_name: string;
variant: Variant;
}

View file

@ -10,7 +10,13 @@ import {
GiftCardSentWebhookPayloadFragment, GiftCardSentWebhookPayloadFragment,
OrderRefundedWebhookPayloadFragment, OrderRefundedWebhookPayloadFragment,
} from "../../../generated/graphql"; } from "../../../generated/graphql";
import { NotifyEventPayload } from "../../pages/api/webhooks/notify"; import {
NotifyPayloadAccountChangeEmailRequest,
NotifyPayloadAccountConfirmation,
NotifyPayloadAccountDelete,
NotifyPayloadAccountPasswordReset,
NotifyPayloadFulfillmentUpdate,
} from "../../lib/notify-event-types";
const exampleOrderPayload: OrderDetailsFragment = { const exampleOrderPayload: OrderDetailsFragment = {
id: "T3JkZXI6NTdiNTBhNDAtYzRmYi00YjQzLWIxODgtM2JhZmRlMTc3MGQ5", id: "T3JkZXI6NTdiNTBhNDAtYzRmYi00YjQzLWIxODgtM2JhZmRlMTc3MGQ5",
@ -167,7 +173,7 @@ const invoiceSentPayload: InvoiceSentWebhookPayloadFragment = {
order: exampleOrderPayload, order: exampleOrderPayload,
}; };
const accountConfirmationPayload: NotifyEventPayload = { const accountConfirmationPayload: NotifyPayloadAccountConfirmation = {
user: { user: {
id: "VXNlcjoxOTY=", id: "VXNlcjoxOTY=",
email: "user@example.com", email: "user@example.com",
@ -189,7 +195,7 @@ const accountConfirmationPayload: NotifyEventPayload = {
logo_url: "", logo_url: "",
}; };
const accountPasswordResetPayload: NotifyEventPayload = { const accountPasswordResetPayload: NotifyPayloadAccountPasswordReset = {
user: { user: {
id: "VXNlcjoxOTY=", id: "VXNlcjoxOTY=",
email: "user@example.com", email: "user@example.com",
@ -211,7 +217,7 @@ const accountPasswordResetPayload: NotifyEventPayload = {
logo_url: "", logo_url: "",
}; };
const accountChangeEmailRequestPayload: NotifyEventPayload = { const accountChangeEmailRequestPayload: NotifyPayloadAccountChangeEmailRequest = {
user: { user: {
id: "VXNlcjoxOTY=", id: "VXNlcjoxOTY=",
email: "user@example.com", email: "user@example.com",
@ -227,15 +233,15 @@ const accountChangeEmailRequestPayload: NotifyEventPayload = {
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6", token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
old_email: "test@example.com1", old_email: "test@example.com1",
new_email: "new.email@example.com1", new_email: "new.email@example.com1",
redirect_url: reset_url:
"http://example.com?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6", "http://example.com/reset?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel", channel_slug: "default-channel",
domain: "demo.saleor.cloud", domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce", site_name: "Saleor e-commerce",
logo_url: "", logo_url: "",
}; };
const accountChangeEmailConfirmPayload: NotifyEventPayload = { const accountChangeEmailConfirmPayload: NotifyPayloadAccountChangeEmailRequest = {
user: { user: {
id: "VXNlcjoxOTY=", id: "VXNlcjoxOTY=",
email: "user@example.com", email: "user@example.com",
@ -248,14 +254,18 @@ const accountChangeEmailConfirmPayload: NotifyEventPayload = {
language_code: "en", language_code: "en",
}, },
recipient_email: "user@example.com", recipient_email: "user@example.com",
old_email: "old@example.com",
new_email: "new@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6", token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
reset_url:
"http://example.com/reset?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel", channel_slug: "default-channel",
domain: "demo.saleor.cloud", domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce", site_name: "Saleor e-commerce",
logo_url: "", logo_url: "",
}; };
const accountDeletePayload: NotifyEventPayload = { const accountDeletePayload: NotifyPayloadAccountDelete = {
user: { user: {
id: "VXNlcjoxOTY=", id: "VXNlcjoxOTY=",
email: "user@example.com", email: "user@example.com",
@ -277,6 +287,184 @@ const accountDeletePayload: NotifyEventPayload = {
logo_url: "", logo_url: "",
}; };
const orderLineMonospaceTeePayloadFragment: NotifyPayloadFulfillmentUpdate["order"]["lines"][0] = {
id: "T3JkZXJMaW5lOjIwMDg4MmMzLWU3NjItNGE0NS05ZjUxLTUyZDAxYTE2ODZjOQ==",
product: {
id: "UHJvZHVjdDoxMzQ=",
attributes: [
{
assignment: {
attribute: {
slug: "material",
name: "Material",
},
},
values: [
{
name: "Cotton",
value: "",
slug: "cotton",
file_url: null,
},
],
},
],
weight: "",
first_image: {
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/4096/",
},
},
images: [
{
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/4096/",
},
},
],
},
product_name: "Monospace Tee",
translated_product_name: "Monospace Tee",
variant_name: "S",
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzQ4",
weight: "",
is_preorder: false,
preorder_end_date: null,
first_image: {
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/4096/",
},
},
images: [
{
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/4096/",
},
},
],
},
translated_variant_name: "S",
product_sku: "328223580",
product_variant_id: "UHJvZHVjdFZhcmlhbnQ6MzQ4",
quantity: 1,
quantity_fulfilled: 1,
currency: "PLN",
unit_price_net_amount: "90.00",
unit_price_gross_amount: "90.00",
unit_tax_amount: "0.00",
total_gross_amount: "90.00",
total_net_amount: "90.00",
total_tax_amount: "0.00",
tax_rate: "0.0000",
is_shipping_required: true,
is_digital: false,
digital_url: null,
unit_discount_value: "0.000",
unit_discount_reason: null,
unit_discount_type: "fixed",
unit_discount_amount: "0.000",
metadata: {},
};
const addressPayloadFragment: NotifyPayloadFulfillmentUpdate["order"]["billing_address"] = {
first_name: "Caitlin",
last_name: "Johnson",
company_name: "",
street_address_1: "8518 Pamela Track Apt. 164",
street_address_2: "",
city: "APRILSHIRE",
city_area: "",
postal_code: "28290",
country: "US",
country_area: "NC",
phone: "",
};
const orderPayloadFragment: NotifyPayloadFulfillmentUpdate["order"] = {
private_metadata: {},
metadata: {},
status: "fulfilled",
language_code: "en",
currency: "PLN",
total_net_amount: "468.68",
undiscounted_total_net_amount: "468.68",
total_gross_amount: "468.68",
undiscounted_total_gross_amount: "468.68",
display_gross_prices: true,
id: "T3JkZXI6MzU4YzcxNTktZmZlYy00ODI3LWI2MzYtYTNmYTEwMTA2MTI5",
token: "358c7159-ffec-4827-b636-a3fa10106129",
number: 231,
channel_slug: "channel-pln",
created: "2023-07-13 10:54:32.527314+00:00",
shipping_price_net_amount: "18.680",
shipping_price_gross_amount: "18.680",
order_details_url: "",
email: "caitlin.johnson@example.com",
subtotal_gross_amount: "450.00",
subtotal_net_amount: "450.00",
tax_amount: "0.00",
lines: [orderLineMonospaceTeePayloadFragment],
billing_address: addressPayloadFragment,
shipping_address: addressPayloadFragment,
shipping_method_name: "FedEx",
collection_point_name: null,
voucher_discount: null,
discount_amount: 0,
};
const fulfillmentPayloadFragment = {
is_tracking_number_url: false,
tracking_number: "1111-1111-1111-1111",
};
const fulfillmentUpdatePayload: NotifyPayloadFulfillmentUpdate = {
fulfillment: fulfillmentPayloadFragment,
order: orderPayloadFragment,
physical_lines: [
{
id: "XXXXXXXX",
order_line: orderLineMonospaceTeePayloadFragment,
quantity: 1,
},
],
digital_lines: [],
recipient_email: "user@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
// TODO: UPDATE WITH BETTER DATA // TODO: UPDATE WITH BETTER DATA
const giftCardSentPayload: GiftCardSentWebhookPayloadFragment = { const giftCardSentPayload: GiftCardSentWebhookPayloadFragment = {
channel: "default_channel", channel: "default_channel",
@ -329,5 +517,6 @@ export const examplePayloads: Record<MessageEventTypes, any> = {
ORDER_CREATED: orderCreatedPayload, ORDER_CREATED: orderCreatedPayload,
ORDER_FULFILLED: orderFulfilledPayload, ORDER_FULFILLED: orderFulfilledPayload,
ORDER_FULLY_PAID: orderFullyPaidPayload, ORDER_FULLY_PAID: orderFullyPaidPayload,
ORDER_FULFILLMENT_UPDATE: fulfillmentUpdatePayload,
ORDER_REFUNDED: orderRefundedPayload, ORDER_REFUNDED: orderRefundedPayload,
}; };

View file

@ -10,6 +10,7 @@ export const messageEventTypes = [
"ORDER_CONFIRMED", "ORDER_CONFIRMED",
"ORDER_CREATED", "ORDER_CREATED",
"ORDER_FULFILLED", "ORDER_FULFILLED",
"ORDER_FULFILLMENT_UPDATE",
"ORDER_FULLY_PAID", "ORDER_FULLY_PAID",
"ORDER_REFUNDED", "ORDER_REFUNDED",
] as const; ] as const;
@ -28,6 +29,7 @@ export const messageEventTypesLabels: Record<MessageEventTypes, string> = {
ORDER_CONFIRMED: "Order confirmed", ORDER_CONFIRMED: "Order confirmed",
ORDER_CREATED: "Order created", ORDER_CREATED: "Order created",
ORDER_FULFILLED: "Order fulfilled", ORDER_FULFILLED: "Order fulfilled",
ORDER_FULFILLMENT_UPDATE: "Order fulfillment updated",
ORDER_FULLY_PAID: "Order fully paid", ORDER_FULLY_PAID: "Order fully paid",
ORDER_REFUNDED: "Order refunded", ORDER_REFUNDED: "Order refunded",
}; };

View file

@ -36,6 +36,42 @@ const addressSection = `<mj-section>
</mj-section> </mj-section>
`; `;
const addressSectionForNotify = `<mj-section>
<mj-column>
<mj-table>
<thead>
<tr>
<th>
Billing address
</th>
<th>
Shipping address
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{{#if order.billing_address}}
{{ order.billing_address.street_address_1 }}
{{else}}
No billing address
{{/if}}
</td>
<td>
{{#if order.shipping_address}}
{{ order.shipping_address.street_address_1}}
{{else}}
No shipping required
{{/if}}
</td>
</tr>
</tbody>
</mj-table>
</mj-column>
</mj-section>
`;
const orderLinesSection = `<mj-section> const orderLinesSection = `<mj-section>
<mj-column> <mj-column>
<mj-table> <mj-table>
@ -95,7 +131,7 @@ const defaultOrderFulfilledMjmlTemplate = `<mjml>
Hello! Hello!
</mj-text> </mj-text>
<mj-text> <mj-text>
Order {{ order.number}} has been fulfilled. Order {{ order.number }} has been fulfilled.
</mj-text> </mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>
@ -129,7 +165,7 @@ const defaultOrderFullyPaidMjmlTemplate = `<mjml>
Hello! Hello!
</mj-text> </mj-text>
<mj-text> <mj-text>
Order {{ order.number}} has been fully paid. Order {{ order.number }} has been fully paid.
</mj-text> </mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>
@ -146,7 +182,7 @@ const defaultOrderRefundedMjmlTemplate = `<mjml>
Hello! Hello!
</mj-text> </mj-text>
<mj-text> <mj-text>
Order {{ order.number}} has been refunded. Order {{ order.number }} has been refunded.
</mj-text> </mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>
@ -163,7 +199,7 @@ const defaultOrderCancelledMjmlTemplate = `<mjml>
Hello! Hello!
</mj-text> </mj-text>
<mj-text> <mj-text>
Order {{ order.number}} has been cancelled. Order {{ order.number }} has been cancelled.
</mj-text> </mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>
@ -290,6 +326,27 @@ const defaultAccountDeleteMjmlTemplate = `<mjml>
</mj-body> </mj-body>
</mjml>`; </mjml>`;
const defaultOrderFulfillmentUpdatedMjmlTemplate = `<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="16px">
Hello!
</mj-text>
<mj-text>
Fulfillment for the order {{ order.number }} has been updated.
</mj-text>
{{#if fulfillment.tracking_number }}
<mj-text>
Tracking number: {{ fulfillment.tracking_number }}
</mj-text>
{{/if}}
</mj-column>
</mj-section>
${addressSectionForNotify}
</mj-body>
</mjml>`;
export const defaultMjmlTemplates: Record<MessageEventTypes, string> = { export const defaultMjmlTemplates: Record<MessageEventTypes, string> = {
ACCOUNT_CHANGE_EMAIL_CONFIRM: defaultAccountChangeEmailConfirmationMjmlTemplate, ACCOUNT_CHANGE_EMAIL_CONFIRM: defaultAccountChangeEmailConfirmationMjmlTemplate,
ACCOUNT_CHANGE_EMAIL_REQUEST: defaultAccountChangeEmailRequestMjmlTemplate, ACCOUNT_CHANGE_EMAIL_REQUEST: defaultAccountChangeEmailRequestMjmlTemplate,
@ -302,6 +359,7 @@ export const defaultMjmlTemplates: Record<MessageEventTypes, string> = {
ORDER_CONFIRMED: defaultOrderConfirmedMjmlTemplate, ORDER_CONFIRMED: defaultOrderConfirmedMjmlTemplate,
ORDER_CREATED: defaultOrderCreatedMjmlTemplate, ORDER_CREATED: defaultOrderCreatedMjmlTemplate,
ORDER_FULFILLED: defaultOrderFulfilledMjmlTemplate, ORDER_FULFILLED: defaultOrderFulfilledMjmlTemplate,
ORDER_FULFILLMENT_UPDATE: defaultOrderFulfillmentUpdatedMjmlTemplate,
ORDER_FULLY_PAID: defaultOrderFullyPaidMjmlTemplate, ORDER_FULLY_PAID: defaultOrderFullyPaidMjmlTemplate,
ORDER_REFUNDED: defaultOrderRefundedMjmlTemplate, ORDER_REFUNDED: defaultOrderRefundedMjmlTemplate,
}; };
@ -318,6 +376,7 @@ export const defaultMjmlSubjectTemplates: Record<MessageEventTypes, string> = {
ORDER_CONFIRMED: "Order {{ order.number }} has been confirmed", ORDER_CONFIRMED: "Order {{ order.number }} has been confirmed",
ORDER_CREATED: "Order {{ order.number }} has been created", ORDER_CREATED: "Order {{ order.number }} has been created",
ORDER_FULFILLED: "Order {{ order.number }} has been fulfilled", ORDER_FULFILLED: "Order {{ order.number }} has been fulfilled",
ORDER_FULFILLMENT_UPDATE: "Fulfillment for order {{ order.number }} has been updated",
ORDER_FULLY_PAID: "Order {{ order.number }} has been fully paid", ORDER_FULLY_PAID: "Order {{ order.number }} has been fully paid",
ORDER_REFUNDED: "Order {{ order.number }} has been refunded", ORDER_REFUNDED: "Order {{ order.number }} has been refunded",
}; };

View file

@ -42,6 +42,7 @@ export const eventToWebhookMapping: Record<MessageEventTypes, AppWebhook> = {
ORDER_FULFILLED: "orderFulfilledWebhook", ORDER_FULFILLED: "orderFulfilledWebhook",
ORDER_FULLY_PAID: "orderFullyPaidWebhook", ORDER_FULLY_PAID: "orderFullyPaidWebhook",
ORDER_REFUNDED: "orderRefundedWebhook", ORDER_REFUNDED: "orderRefundedWebhook",
ORDER_FULFILLMENT_UPDATE: "notifyWebhook",
}; };
const logger = createLogger({ const logger = createLogger({

View file

@ -1,4 +1,4 @@
import { AppWebhook, AppWebhooks } from "./webhook-management-service"; import { AppWebhook } from "./webhook-management-service";
export type WebhookStatuses = Record<AppWebhook, boolean>; export type WebhookStatuses = Record<AppWebhook, boolean>;

View file

@ -2,67 +2,13 @@ import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handl
import { saleorApp } from "../../../saleor-app"; import { saleorApp } from "../../../saleor-app";
import { createLogger, createGraphQLClient } from "@saleor/apps-shared"; import { createLogger, createGraphQLClient } from "@saleor/apps-shared";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages"; import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { MessageEventTypes } from "../../../modules/event-handlers/message-event-types"; import { NotifySubscriptionPayload, notifyEventMapping } from "../../../lib/notify-event-types";
// Notify webhook event groups multiple event types under the one webhook. We need to map it to events recognized by the App
const notifyEventMapping: Record<string, MessageEventTypes> = {
account_confirmation: "ACCOUNT_CONFIRMATION",
account_delete: "ACCOUNT_DELETE",
account_password_reset: "ACCOUNT_PASSWORD_RESET",
account_change_email_request: "ACCOUNT_CHANGE_EMAIL_REQUEST",
account_change_email_confirm: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
};
/* /*
* Notify event handles multiple event types which are recognized based on payload field `notify_event`. * The Notify webhook is triggered on multiple Saleor events.
* Handler recognizes if event is one of the supported typed and sends appropriate message. * Type of the message is determined by `notify_event` field in the payload.
*/ */
interface NotifySubscriptionPayload {
notify_event: string;
payload: NotifyEventPayload;
meta: Meta;
}
interface Meta {
issued_at: Date;
version: string;
issuing_principal: IssuingPrincipal;
}
interface IssuingPrincipal {
id: null | string;
type: null | string;
}
export interface NotifyEventPayload {
user: User;
recipient_email: string;
channel_slug: string;
domain: string;
site_name: string;
logo_url: string;
token?: string;
confirm_url?: string;
reset_url?: string;
delete_url?: string;
old_email?: string;
new_email?: string;
redirect_url?: string;
}
interface User {
id: string;
email: string;
first_name: string;
last_name: string;
is_staff: boolean;
is_active: boolean;
private_metadata: Record<string, string>;
metadata: Record<string, string>;
language_code: string;
}
export const notifyWebhook = new SaleorAsyncWebhook<NotifySubscriptionPayload>({ export const notifyWebhook = new SaleorAsyncWebhook<NotifySubscriptionPayload>({
name: "notify", name: "notify",
webhookPath: "api/webhooks/notify", webhookPath: "api/webhooks/notify",
@ -89,10 +35,10 @@ const handler: NextWebhookApiHandler<NotifySubscriptionPayload> = async (req, re
.json({ error: "Email recipient has not been specified in the event payload." }); .json({ error: "Email recipient has not been specified in the event payload." });
} }
// Since NOTIFY can be send on events unrelated to this app, lack of mapping means the App does not support it
const event = notifyEventMapping[payload.notify_event]; const event = notifyEventMapping[payload.notify_event];
if (!event) { if (!event) {
// NOTIFY webhook sends multiple events to the same endpoint. The app supports only a subset of them.
logger.debug(`The type of received notify event (${payload.notify_event}) is not supported.`); logger.debug(`The type of received notify event (${payload.notify_event}) is not supported.`);
return res.status(200).json({ message: `${payload.notify_event} event is not supported.` }); return res.status(200).json({ message: `${payload.notify_event} event is not supported.` });
} }