Technical proposal · for Joy dev team · 2026-05-27
Tách validation logic ra một package Zod schema thuần, dùng chung cho frontend và REST v2 backend — giữ nguyên message i18n và không đụng tới component frontend.
Validation hiện chỉ sống ở frontend (packages/assets/src/config/validations/), viết theo rules-object của @avada/react-form-hook và dính chặt i18n. Backend REST v2 phải tự viết lại → drift, dễ sót, đã sinh ra bug bảo mật/data-integrity.
Đề xuất: đưa logic validation về một package shared-schemas dạng Zod schema thuần (không React, không i18n instance), để cả FE adapter lẫn BE handler import cùng một nguồn. Một adapter mỏng (~50 dòng) giữ cho frontend hoạt động y nguyên.
program.js ~2165 dòng, cùng membershipPlanRules, referralProgram, tierProgram... đều là rules-object cho form hook, coupled với i18n callback của Shopify React I18n.Schema là nguồn duy nhất; mỗi phía có một presenter riêng (FE adapter → form hook rules; BE → HTTP errors).
| Ràng buộc | Cách đạt được |
|---|---|
| Logic dùng chung FE+BE | Schema thuần ở shared-schemas, cả 2 phía import cùng nguồn. |
| Message i18n giữ nguyên | Schema chỉ đánh dấu key qua params.i18n; translate ở presenter; giữ format Validations.<tab>.<key> — file dịch không đụng. |
| Frontend zero-touch | File *Rules.js giữ export signature, chỉ rewrite internal. Component + useFormHook không đổi 1 dòng. |
| Quyết định | Phương án chọn | Vì sao |
|---|---|---|
| Validation library | Zod v4 | Built-in vi locale + 40 langs; không add lib thứ 3; bundle FE +~14kB. |
| Migration style | Strangler fig + anti-corruption layer | Adapter giữ react-form-hook → migrate module-by-module, không big-bang, rollback per-module. |
| BE error response | Structured {code, path, params} | Client tự dịch; không leak Zod internals; partner API dễ consume. (cần team confirm — xem mục 7) |
| i18n ownership | Presenter layer, không phải schema | Schema thuần reuse được BE; message vẫn theo locale của shop. |
Schema thuần (shared package)
// packages/shared-schemas/src/schemas/membershipPlan.js export const planSchema = z.object({ title: z.string().min(1, { params: { i18n: { key: 'titleRequired' } } }), tiers: z.array(tierSchema).superRefine((tiers, ctx) => { // tier overlap → ctx.addIssue({ path: [i, 'fromOrder'], params: {...} }) }) })
Adapter Zod → react-form-hook rules (~50 dòng, shared)
export const zodToFormHookRules = (schema, data, i18n, opts) => {
const result = schema.safeParse(data)
if (result.success) return {}
// map issues → { 'plan/0/title': { value, validation: 'custom', message } }
}
File rules cũ — internal rewrite, signature GIỮ NGUYÊN
// packages/assets/src/config/validations/membershipPlanRules.js import { plansListSchema } from '@avada/shared-schemas/schemas/membershipPlan' export const membershipPlanRules = (plans, i18n) => zodToFormHookRules(plansListSchema, plans, i18n, { tabKey: 'membershipPlanRules', keyPrefix: 'plan/' }) // → component import path cũ, gọi cùng cách, nhận shape errors cũ.
Backend REST v2 — dùng schema trực tiếp
const result = plansListSchema.safeParse(req.body.plans)
if (!result.success) return res.status(422).json({
errors: result.error.issues // { code, path, params } — client tự dịch
})
Mọi pattern đều emit issue có path[] → adapter convert đồng nhất, không cần thay đổi.
| Pattern Joy | Công cụ Zod |
|---|---|
| Tier overlap / array invariant | .superRefine() với path: [i, field] |
Variant theo event type (program.js) | z.discriminatedUnion('event', [...]) + exhaustiveness check |
| Composite value (val + unit) | discriminated union / superRefine |
Conditional depends() | .optional() + superRefine, hoặc discriminated union |
| External context (shop plan, feature flag) | Schema factory makeXSchema(ctx) — build per-request |
| Async (uniqueness DB) | .refine(async) + parseAsync — BE-only layer |
| Phase | Scope | Effort |
|---|---|---|
| 0 — POC | Tạo workspace + migrate appleWalletRules.js (30 dòng); snapshot test error message giống hệt; verify BE chạy cùng schema. | ~1 ngày |
| 1 — Membership Plan | Schema phức tạp nhất (tier overlap, prepaid/postpaid); wire BE REST v2 cùng schema; test parity payload sai. | 2-3 ngày |
| 2 — Mở rộng (parallel) | notificationRules, emailReportRules, referralProgram, tierProgram; program.js (2165 dòng, 30+ variant) chia subschema. | 1-2 ngày/module · program.js ~5-7 ngày |
Mỗi phase ship + rollback độc lập per-module. Không big-bang rewrite.
program.js không? Cần sync để tránh conflict khi migrate.{code, path, params} (client dịch) hay translated string?@avada/react-form-hook bằng useForm + Zod resolver?depends() đọc state ngoài form data → confirm schema factory là cách team chấp nhận.