order-notice.js 6.74 KB
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('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;')
}