Skip to content

Commit 80bfcfc

Browse files
authored
Merge pull request #2 from codee-sh/feat/add-transforms
Feat: Add transforms
2 parents d8b8a45 + cb597d8 commit 80bfcfc

52 files changed

Lines changed: 1766 additions & 273 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/petite-jars-sniff.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@codee-sh/medusa-plugin-notification-emails": patch
3+
---
4+
5+
Data transformation system: Centralized transformers for converting raw API data
6+
(orders, products, etc.) into email-friendly format with formatted dates, amounts,
7+
and addresses

.changeset/tiny-aliens-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@codee-sh/medusa-plugin-notification-emails": patch
3+
---
4+
5+
Added ORDER_ATTRIBUTES, PRODUCT_ATTRIBUTES, and QUERY_FIELDS definitions for all entity types (order, product, variant, inventory, etc.)

.changeset/wild-cases-boil.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@codee-sh/medusa-plugin-notification-emails": patch
3+
---
4+
5+
Moved order email templates to order/ subdirectory (placed, completed, updated) for better organization

src/admin/notifications-templates/groups/contact-form/contact-form.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ import { contactFormMockData } from "../../../../../emails-previews/contact-form
55
import { TEMPLATES_NAMES } from "../../../../templates/emails";
66

77
export const ContactFormTemplate = () => {
8-
const [templateData, setTemplateData] = useState<any>(null);
9-
const [previewData, setPreviewData] = useState<any>(null);
8+
const [context, setContext] = useState<any>(null);
9+
const [previewContext, setPreviewData] = useState<any>(null);
1010

1111
useEffect(() => {
12-
setTemplateData(contactFormMockData);
12+
setContext({
13+
contact_form: contactFormMockData
14+
});
1315
}, []);
1416

1517
const { data: preview, isLoading: isPreviewLoading } = usePreview({
1618
templateName: TEMPLATES_NAMES.CONTACT_FORM,
17-
templateData: templateData,
19+
context: context,
20+
contextType: "contact_form",
1821
locale: "pl",
19-
enabled: !!templateData,
20-
extraKey: [templateData],
22+
enabled: !!context,
23+
extraKey: [context],
2124
});
2225

2326
useEffect(() => {
@@ -31,10 +34,10 @@ export const ContactFormTemplate = () => {
3134
{isPreviewLoading && (
3235
<Alert variant="info">Loading preview...</Alert>
3336
)}
34-
{previewData && (
37+
{previewContext && (
3538
<div className="px-6 py-4">
3639
<iframe
37-
srcDoc={previewData?.html || ""}
40+
srcDoc={previewContext?.html || ""}
3841
style={{ width: "100%", border: "none", minHeight: "600px" }}
3942
sandbox="allow-same-origin"
4043
/>

src/admin/notifications-templates/groups/order/order-placed.tsx

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { useEffect, useState } from "react";
22
import { Alert } from "@medusajs/ui"
33
import { useOrder } from "../../../../hooks/api/orders";
44
import { usePreview } from "../../../../hooks/api/preview";
5-
import { getFormattedAddress, formatDate, getLocaleAmount, getTotalCaptured } from "../../../../utils";
65
import { TEMPLATES_NAMES } from "../../../../templates/emails";
76

87
export const OrderPlacedTemplate = ({ orderId }: { orderId: string }) => {
9-
const [templateData, setTemplateData] = useState<any>(null);
10-
const [previewData, setPreviewData] = useState<any>(null);
8+
const [context, setContext] = useState<any>(null);
9+
const [previewContext, setPreviewData] = useState<any>(null);
1110

1211
const { data: order, isLoading: isOrderLoading } = useOrder({
1312
order_id: orderId,
@@ -16,47 +15,19 @@ export const OrderPlacedTemplate = ({ orderId }: { orderId: string }) => {
1615

1716
useEffect(() => {
1817
if (order?.display_id) {
19-
const shippingAddressText = getFormattedAddress({ address: order.shipping_address }).join("<br/>");
20-
const billingAddressText = getFormattedAddress({ address: order.billing_address }).join("<br/>");
21-
const templateData: any = {
22-
orderNumber: `#${order.display_id}`,
23-
customerName: order.email,
24-
customerEmail: order.email,
25-
orderDate: formatDate({ date: order.created_at, includeTime: true, localeCode: "pl" }),
26-
totalAmount: order.items.reduce((acc, item) => acc + (item.variant?.prices?.[0]?.amount || 0) * item.quantity, 0),
27-
currency_code: order.currency_code,
28-
items: order.items.map((item) => ({
29-
thumbnail: item.thumbnail == null ? "" : item.thumbnail,
30-
title: item.title,
31-
quantity: item.quantity,
32-
price: getLocaleAmount(item.unit_price, order.currency_code),
33-
})),
34-
shippingAddress: shippingAddressText,
35-
billingAddress: billingAddressText,
36-
summary: {
37-
total: getLocaleAmount(order.summary.original_order_total, order.currency_code),
38-
paid_total: getLocaleAmount(getTotalCaptured(order.payment_collections || []), order.currency_code),
39-
tax_total: getLocaleAmount(order.tax_total, order.currency_code),
40-
discount_total: getLocaleAmount(order.discount_total, order.currency_code)
41-
},
42-
sales_channel: {
43-
name: order?.sales_channel?.name,
44-
description: order?.sales_channel?.description,
45-
}
46-
};
47-
48-
console.log(order);
49-
50-
setTemplateData(templateData);
18+
setContext({
19+
order: order
20+
});
5121
}
5222
}, [order]);
5323

5424
const { data: preview, isLoading: isPreviewLoading } = usePreview({
5525
templateName: TEMPLATES_NAMES.ORDER_PLACED,
56-
templateData: templateData,
26+
context: context,
27+
contextType: "order",
5728
locale: "pl",
58-
enabled: !!templateData,
59-
extraKey: [templateData, orderId]
29+
enabled: !!context,
30+
extraKey: [context, orderId]
6031
});
6132

6233
useEffect(() => {
@@ -74,9 +45,9 @@ export const OrderPlacedTemplate = ({ orderId }: { orderId: string }) => {
7445
return (
7546
<div className="px-6 py-4">
7647
{isOrderLoading && <Alert variant="info">Loading order {orderId}...</Alert>}
77-
{previewData && <div className="px-6 py-4">
48+
{previewContext && <div className="px-6 py-4">
7849
<iframe
79-
srcDoc={previewData?.html || ""}
50+
srcDoc={previewContext?.html || ""}
8051
style={{ width: '100%', border: 'none', minHeight: '600px' }}
8152
sandbox="allow-same-origin"
8253
/>

src/api/admin/notification-plugin/render-template/route.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
22
import { MedusaError } from "@medusajs/framework/utils"
3-
import { getPluginOptions } from "../../../../utils/plugins"
43
import { defaultTheme } from "../../../../templates/shared/theme"
54
import { renderTemplate, TemplateName, TemplateData } from "../../../../templates/emails"
5+
import { transformContext } from "../../../../utils/transforms"
6+
import { getPluginOptions } from "../../../../utils/plugins"
67

78
export async function POST(
8-
req: MedusaRequest<{ templateName: string, templateData: any, locale: string }>,
9+
req: MedusaRequest<{ templateName: string, context: any, contextType: any, locale: string }>,
910
res: MedusaResponse
1011
) {
1112
const pluginOptions = getPluginOptions(req.scope, "@codee-sh/medusa-plugin-notification")
1213

1314
const templateName = req.body?.templateName as TemplateName
14-
const templateData = req.body?.templateData as TemplateData
15+
const context = req.body?.context as TemplateData
16+
const contextType = req.body?.contextType
17+
1518
const locale = req.body?.locale || "pl"
1619

17-
if (!templateName || !templateData || !locale) {
20+
if (!templateName || !context || !locale) {
1821
throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, "Template name, template data and locale are required")
1922
}
2023

24+
const transformedTemplateData = transformContext(contextType, context, locale)
25+
2126
const { html, text } = await renderTemplate(
2227
templateName,
23-
templateData,
28+
transformedTemplateData,
2429
{
2530
locale: locale,
2631
theme: pluginOptions?.theme || defaultTheme,

src/hooks/api/preview.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { sdk } from "../../admin/lib/sdk"
1111

1212
export type UsePreviewParams = {
1313
templateName?: string
14-
templateData?: any
14+
context?: any
15+
contextType?: any
1516
locale?: string
1617
extraKey?: unknown[]
1718
enabled?: boolean
@@ -36,7 +37,8 @@ export const usePreview = (
3637
) => {
3738
const {
3839
templateName,
39-
templateData,
40+
context,
41+
contextType,
4042
locale,
4143
extraKey = [],
4244
enabled = false,
@@ -61,13 +63,14 @@ export const usePreview = (
6163
method: "POST",
6264
body: {
6365
templateName: templateName,
64-
templateData: templateData,
66+
context: context,
67+
contextType: contextType,
6568
locale: locale,
6669
},
6770
})
6871
},
6972
staleTime: 0,
70-
enabled: enabled && !!templateName && !!templateData && !!locale,
73+
enabled: enabled && !!templateName && !!context && !!locale,
7174
...(options as any),
7275
})
7376

src/subscribers/order-completed.ts

Lines changed: 13 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {
22
SubscriberArgs,
33
type SubscriberConfig,
44
} from "@medusajs/medusa"
5-
import { Modules, ContainerRegistrationKeys, MedusaError } from "@medusajs/framework/utils"
6-
import { renderTemplate } from "@codee-sh/medusa-plugin-notification-emails/templates/emails"
7-
import { TEMPLATES_NAMES } from "@codee-sh/medusa-plugin-notification-emails/templates/emails/types"
8-
import { formatDate, getFormattedAddress, getLocaleAmount, getTotalCaptured } from "@codee-sh/medusa-plugin-notification-emails/utils"
9-
import { getPluginOptions } from "@codee-sh/medusa-plugin-notification-emails/utils/plugins"
5+
import { Modules, MedusaError } from "@medusajs/framework/utils"
6+
import { renderTemplate } from "../templates/emails"
7+
import { TEMPLATES_NAMES } from "../templates/emails/types"
8+
import { transformContext } from "../utils/transforms"
9+
import { getPluginOptions } from "../utils/plugins"
10+
import { getOrderByIdWorkflow } from "../workflows/order/get-order-by-id"
1011

11-
export default async function orderCompletedEmailsHandler({
12+
export default async function orderPlacedEmailsHandler({
1213
event: { data: { id, trigger_type } },
1314
container,
1415
}: SubscriberArgs<{ id: string, trigger_type: string }>) {
@@ -17,73 +18,24 @@ export default async function orderCompletedEmailsHandler({
1718
const notificationModuleService = container.resolve(
1819
Modules.NOTIFICATION
1920
)
20-
const query = container.resolve(ContainerRegistrationKeys.QUERY)
2121
const triggerType = trigger_type || 'system'
2222

2323
if (!id) {
2424
throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, "Order ID is required")
2525
}
2626

27-
const { data: [order] } = await query.graph({
28-
entity: "order",
29-
fields: [
30-
"id",
31-
"email",
32-
"created_at",
33-
"updated_at",
34-
"payment_collections.*",
35-
"items.*",
36-
"items.variant.*",
37-
"items.variant.product.*",
38-
"currency_code",
39-
"display_id",
40-
"sales_channel.name",
41-
"sales_channel.description",
42-
"shipping_address.*",
43-
"billing_address.*",
44-
"summary.*",
45-
"tax_total",
46-
"discount_total",
47-
],
48-
filters: {
49-
id: id,
27+
const { result: order } = await getOrderByIdWorkflow(container).run({
28+
input: {
29+
order_id: id,
5030
},
5131
})
5232

5333
if (!order) {
5434
return
5535
}
5636

57-
const shippingAddressText = getFormattedAddress({ address: order.shipping_address }).join("<br/>");
58-
const billingAddressText = getFormattedAddress({ address: order.billing_address }).join("<br/>");
59-
const templateData = {
60-
sales_channel: {
61-
name: order?.sales_channel?.name,
62-
description: order?.sales_channel?.description,
63-
},
64-
orderNumber: `#${order.display_id}`,
65-
customerName: order.email,
66-
customerEmail: order.email,
67-
orderDate: formatDate({ date: order.created_at, includeTime: true, localeCode: "pl" }),
68-
completedDate: formatDate({ date: order.updated_at || order.created_at, includeTime: true, localeCode: "pl" }),
69-
totalAmount: order.items.reduce((acc, item) => acc + (item.variant?.prices?.[0]?.amount || 0) * item.quantity, 0),
70-
currency: order.currency_code,
71-
items: order.items.map((item) => ({
72-
thumbnail: item.thumbnail,
73-
title: item.title,
74-
quantity: item.quantity,
75-
price: getLocaleAmount(item.unit_price, order.currency_code),
76-
})),
77-
shippingAddress: shippingAddressText,
78-
billingAddress: billingAddressText,
79-
summary: {
80-
total: getLocaleAmount(order.summary.original_order_total, order.currency_code),
81-
paid_total: getLocaleAmount(getTotalCaptured(order.payment_collections || []), order.currency_code),
82-
tax_total: getLocaleAmount(order.tax_total, order.currency_code),
83-
discount_total: getLocaleAmount(order.discount_total, order.currency_code),
84-
currency_code: order.currency_code,
85-
}
86-
};
37+
// Transform raw order data to email template format
38+
const templateData = transformContext("order", order, "pl")
8739

8840
const templateName = TEMPLATES_NAMES.ORDER_COMPLETED
8941

@@ -97,7 +49,7 @@ export default async function orderCompletedEmailsHandler({
9749
)
9850

9951
const result = await notificationModuleService.createNotifications({
100-
to: order.email,
52+
to: order.order.customer.email,
10153
channel: "email",
10254
template: templateName,
10355
trigger_type: triggerType,

0 commit comments

Comments
 (0)