order-notice.js
6.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { EmailError } from '../map-error.js'
const COPY = Object.freeze({
zh: {
purposes: {
order_paid: ['订单已完成', '我们已收到您的付款。'],
subscription_active: ['订阅已生效', '您的 VOKO 订阅现已生效。'],
subscription_canceled: ['订阅已取消', '您的 VOKO 订阅已取消。'],
order_refunded: ['订单已退款', '您的订单退款已处理。'],
order_disputed: ['订单正在审核', '您的订单存在争议,目前正在审核。'],
},
order: '订单编号', product: '商品', amount: '金额', status: '状态', footer: '感谢您使用 VOKO。',
statuses: { pending: '待支付', paid: '已支付', refunded: '已退款', disputed: '争议处理中' },
},
en: {
purposes: {
order_paid: ['Your order is complete', 'We have received your payment.'],
subscription_active: ['Your subscription is active', 'Your VOKO subscription is now active.'],
subscription_canceled: ['Your subscription was canceled', 'Your VOKO subscription has been canceled.'],
order_refunded: ['Your order was refunded', 'Your order refund has been processed.'],
order_disputed: ['Your order is under review', 'A dispute was opened for your order and is now under review.'],
},
order: 'Order', product: 'Product', amount: 'Amount', status: 'Status', footer: 'Thank you for using VOKO.',
statuses: { pending: 'Pending', paid: 'Paid', refunded: 'Refunded', disputed: 'Under review' },
},
ja: {
purposes: {
order_paid: ['ご注文が完了しました', 'お支払いを確認しました。'],
subscription_active: ['サブスクリプションが有効になりました', 'VOKOのサブスクリプションをご利用いただけます。'],
subscription_canceled: ['サブスクリプションが解約されました', 'VOKOのサブスクリプションが解約されました。'],
order_refunded: ['ご注文の返金が完了しました', 'ご注文の返金手続きが完了しました。'],
order_disputed: ['ご注文を確認中です', 'ご注文に対する異議申し立てがあり、現在確認中です。'],
},
order: '注文番号', product: '商品', amount: '金額', status: 'ステータス', footer: 'VOKOをご利用いただきありがとうございます。',
statuses: { pending: '支払い待ち', paid: '支払い済み', refunded: '返金済み', disputed: '確認中' },
},
ko: {
purposes: {
order_paid: ['주문이 완료되었습니다', '결제가 확인되었습니다.'],
subscription_active: ['구독이 활성화되었습니다', 'VOKO 구독이 활성화되었습니다.'],
subscription_canceled: ['구독이 취소되었습니다', 'VOKO 구독이 취소되었습니다.'],
order_refunded: ['주문이 환불되었습니다', '주문 환불 처리가 완료되었습니다.'],
order_disputed: ['주문을 검토 중입니다', '주문에 대한 이의 제기가 접수되어 현재 검토 중입니다.'],
},
order: '주문 번호', product: '상품', amount: '금액', status: '상태', footer: 'VOKO를 이용해 주셔서 감사합니다.',
statuses: { pending: '결제 대기', paid: '결제 완료', refunded: '환불 완료', disputed: '검토 중' },
},
es: {
purposes: {
order_paid: ['Tu pedido se ha completado', 'Hemos recibido tu pago.'],
subscription_active: ['Tu suscripción está activa', 'Tu suscripción a VOKO ya está activa.'],
subscription_canceled: ['Tu suscripción fue cancelada', 'Tu suscripción a VOKO ha sido cancelada.'],
order_refunded: ['Tu pedido fue reembolsado', 'El reembolso de tu pedido ha sido procesado.'],
order_disputed: ['Tu pedido está en revisión', 'Se ha abierto una disputa sobre tu pedido y está en revisión.'],
},
order: 'Pedido', product: 'Producto', amount: 'Importe', status: 'Estado', footer: 'Gracias por usar VOKO.',
statuses: { pending: 'Pendiente', paid: 'Pagado', refunded: 'Reembolsado', disputed: 'En revisión' },
},
pt: {
purposes: {
order_paid: ['Seu pedido foi concluído', 'Recebemos seu pagamento.'],
subscription_active: ['Sua assinatura está ativa', 'Sua assinatura do VOKO está ativa.'],
subscription_canceled: ['Sua assinatura foi cancelada', 'Sua assinatura do VOKO foi cancelada.'],
order_refunded: ['Seu pedido foi reembolsado', 'O reembolso do seu pedido foi processado.'],
order_disputed: ['Seu pedido está em análise', 'Uma contestação foi aberta para seu pedido, que agora está em análise.'],
},
order: 'Pedido', product: 'Produto', amount: 'Valor', status: 'Status', footer: 'Obrigado por usar o VOKO.',
statuses: { pending: 'Pendente', paid: 'Pago', refunded: 'Reembolsado', disputed: 'Em análise' },
},
})
export function render(data) {
if (!data || typeof data !== 'object') fail('data is required')
const copy = COPY[data.locale]
if (!copy) fail(`unsupported locale: ${String(data.locale)}`)
const purposeCopy = copy.purposes[data.purpose]
if (!purposeCopy) fail(`unsupported purpose: ${String(data.purpose)}`)
const orderId = requiredText(data.orderId, 'orderId')
const productId = requiredText(data.productId, 'productId')
const status = copy.statuses[data.status]
if (!status) fail(`unsupported status: ${String(data.status)}`)
const amount = optionalText(data.amount)
const [subject, intro] = purposeCopy
const details = [
`${copy.order}: ${orderId}`,
`${copy.product}: ${productId}`,
amount ? `${copy.amount}: ${amount}` : null,
`${copy.status}: ${status}`,
].filter(Boolean)
const text = [subject, '', intro, '', ...details, '', copy.footer].join('\n')
const html = [
'<!doctype html><html><body style="font-family:Arial,Helvetica,sans-serif;color:#1a1a1a;line-height:1.6;">',
`<h2 style="margin:0 0 16px;">${escapeHtml(subject)}</h2>`,
`<p style="margin:0 0 16px;">${escapeHtml(intro)}</p>`,
...details.map(detail => `<p style="margin:0 0 8px;">${escapeHtml(detail)}</p>`),
`<p style="margin:16px 0 0;color:#666;">${escapeHtml(copy.footer)}</p>`,
'</body></html>',
].join('')
return { subject, html, text }
}
function requiredText(value, field) {
if (typeof value === 'string' && value.trim()) return value.trim()
fail(`missing field "${field}"`)
}
function optionalText(value) {
if (value === undefined || value === null || value === '') return ''
return requiredText(value, 'amount')
}
function fail(message) {
throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: `order-notice: ${message}` })
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''')
}