From d9f159a8c3ef4dc151ada5efe3552294d5ffb67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B4ng=20=C4=90=E1=BB=A9c=20Huy?= Date: Thu, 13 Aug 2026 13:34:24 +0700 Subject: [PATCH] Stage M7 --- README.md | 28 +- .../src/app/(dashboard)/cms/[id]/page.tsx | 37 + apps/admin/src/app/(dashboard)/cms/page.tsx | 18 +- .../src/app/(dashboard)/coupons/page.tsx | 22 - .../src/app/(dashboard)/discounts/page.tsx | 24 + .../src/app/(dashboard)/promotions/page.tsx | 22 - .../src/app/(dashboard)/reviews/page.tsx | 18 +- .../admin/src/features/cms/content-editor.tsx | 361 +++++++ apps/admin/src/features/cms/content-table.tsx | 190 ++++ .../features/discounts/discount-dialog.tsx | 536 +++++++++++ .../features/discounts/discounts-table.tsx | 296 ++++++ .../src/features/reviews/reviews-queue.tsx | 251 +++++ apps/admin/src/lib/navigation.ts | 4 +- apps/admin/src/messages/en.json | 168 +++- apps/admin/src/messages/vi.json | 168 +++- .../migration.sql | 122 +++ .../20260813025434_add_reviews/migration.sql | 57 ++ .../20260813041114_add_content/migration.sql | 55 ++ apps/api/prisma/schema.prisma | 351 ++++++- .../api/src/modules/carts/carts.controller.ts | 24 + apps/api/src/modules/carts/carts.module.ts | 3 +- apps/api/src/modules/carts/carts.service.ts | 98 +- .../src/modules/checkout/checkout.module.ts | 3 +- .../src/modules/checkout/checkout.service.ts | 16 + apps/api/src/modules/cms/cms.controller.ts | 137 +++ apps/api/src/modules/cms/cms.module.ts | 32 +- apps/api/src/modules/cms/cms.repository.ts | 153 +++ apps/api/src/modules/cms/cms.service.ts | 315 +++++++ apps/api/src/modules/cms/public/index.ts | 4 +- apps/api/src/modules/orders/orders.module.ts | 3 + apps/api/src/modules/orders/orders.service.ts | 5 + .../src/modules/products/products.mapper.ts | 25 +- .../modules/products/products.repository.ts | 6 + .../promotions/discount-engine.spec.ts | 158 ++++ .../src/modules/promotions/discount-engine.ts | 151 +++ .../promotions/promotions-admin.service.ts | 244 +++++ .../promotions/promotions.controller.ts | 84 ++ .../modules/promotions/promotions.module.ts | 28 +- .../modules/promotions/promotions.service.ts | 214 +++++ .../src/modules/promotions/public/index.ts | 3 +- apps/api/src/modules/reviews/public/index.ts | 2 +- .../modules/reviews/review-summary.spec.ts | 78 ++ .../src/modules/reviews/reviews.controller.ts | 110 +++ .../api/src/modules/reviews/reviews.module.ts | 30 +- .../src/modules/reviews/reviews.repository.ts | 182 ++++ .../src/modules/reviews/reviews.service.ts | 342 +++++++ apps/storefront/package.json | 2 + .../app/[locale]/(shop)/blog/[slug]/page.tsx | 112 +++ .../src/app/[locale]/(shop)/blog/page.tsx | 78 +- .../app/[locale]/(shop)/pages/[slug]/page.tsx | 63 ++ .../[locale]/(shop)/products/[slug]/page.tsx | 3 + .../src/components/commerce/cart-view.tsx | 12 + .../src/components/commerce/checkout-form.tsx | 6 + .../src/components/commerce/discount-form.tsx | 109 +++ .../src/components/commerce/markdown.tsx | 113 +++ .../commerce/order-confirmation.tsx | 4 + .../components/commerce/product-reviews.tsx | 142 +++ .../src/components/commerce/review-form.tsx | 232 +++++ .../src/components/layout/site-footer.tsx | 40 +- .../src/features/cart/cart-provider.tsx | 31 +- apps/storefront/src/lib/routes.ts | 2 + apps/storefront/src/messages/en.json | 48 +- apps/storefront/src/messages/vi.json | 48 +- ...0-promotions-and-coupons-are-one-entity.md | 99 ++ ...1-a-review-is-anchored-to-an-order-line.md | 102 ++ ...-content-is-markdown-not-a-page-builder.md | 102 ++ docs/adr/README.md | 3 + packages/api-client/src/create-client.ts | 26 + packages/api-client/src/index.ts | 22 + packages/api-client/src/resources/commerce.ts | 11 + packages/api-client/src/resources/content.ts | 116 +++ .../src/resources/promotions-admin.ts | 70 ++ packages/api-client/src/resources/reviews.ts | 108 +++ packages/types/src/auth/permissions.ts | 6 +- packages/types/src/commerce/cart.ts | 6 + packages/types/src/commerce/discount.ts | 71 ++ packages/types/src/commerce/review.ts | 89 ++ packages/types/src/content/entry.ts | 68 ++ packages/types/src/index.ts | 3 + packages/validation/src/commerce.ts | 114 +++ packages/validation/src/content.ts | 52 ++ packages/validation/src/index.ts | 1 + pnpm-lock.yaml | 878 +++++++++++++++++- pnpm-workspace.yaml | 2 + 84 files changed, 7992 insertions(+), 180 deletions(-) create mode 100644 apps/admin/src/app/(dashboard)/cms/[id]/page.tsx delete mode 100644 apps/admin/src/app/(dashboard)/coupons/page.tsx create mode 100644 apps/admin/src/app/(dashboard)/discounts/page.tsx delete mode 100644 apps/admin/src/app/(dashboard)/promotions/page.tsx create mode 100644 apps/admin/src/features/cms/content-editor.tsx create mode 100644 apps/admin/src/features/cms/content-table.tsx create mode 100644 apps/admin/src/features/discounts/discount-dialog.tsx create mode 100644 apps/admin/src/features/discounts/discounts-table.tsx create mode 100644 apps/admin/src/features/reviews/reviews-queue.tsx create mode 100644 apps/api/prisma/migrations/20260812151048_add_discounts/migration.sql create mode 100644 apps/api/prisma/migrations/20260813025434_add_reviews/migration.sql create mode 100644 apps/api/prisma/migrations/20260813041114_add_content/migration.sql create mode 100644 apps/api/src/modules/cms/cms.controller.ts create mode 100644 apps/api/src/modules/cms/cms.repository.ts create mode 100644 apps/api/src/modules/cms/cms.service.ts create mode 100644 apps/api/src/modules/promotions/discount-engine.spec.ts create mode 100644 apps/api/src/modules/promotions/discount-engine.ts create mode 100644 apps/api/src/modules/promotions/promotions-admin.service.ts create mode 100644 apps/api/src/modules/promotions/promotions.controller.ts create mode 100644 apps/api/src/modules/promotions/promotions.service.ts create mode 100644 apps/api/src/modules/reviews/review-summary.spec.ts create mode 100644 apps/api/src/modules/reviews/reviews.controller.ts create mode 100644 apps/api/src/modules/reviews/reviews.repository.ts create mode 100644 apps/api/src/modules/reviews/reviews.service.ts create mode 100644 apps/storefront/src/app/[locale]/(shop)/blog/[slug]/page.tsx create mode 100644 apps/storefront/src/app/[locale]/(shop)/pages/[slug]/page.tsx create mode 100644 apps/storefront/src/components/commerce/discount-form.tsx create mode 100644 apps/storefront/src/components/commerce/markdown.tsx create mode 100644 apps/storefront/src/components/commerce/product-reviews.tsx create mode 100644 apps/storefront/src/components/commerce/review-form.tsx create mode 100644 docs/adr/0020-promotions-and-coupons-are-one-entity.md create mode 100644 docs/adr/0021-a-review-is-anchored-to-an-order-line.md create mode 100644 docs/adr/0022-editorial-content-is-markdown-not-a-page-builder.md create mode 100644 packages/api-client/src/resources/content.ts create mode 100644 packages/api-client/src/resources/promotions-admin.ts create mode 100644 packages/api-client/src/resources/reviews.ts create mode 100644 packages/types/src/commerce/discount.ts create mode 100644 packages/types/src/commerce/review.ts create mode 100644 packages/types/src/content/entry.ts create mode 100644 packages/validation/src/content.ts diff --git a/README.md b/README.md index 8c13f01..7314a41 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ sport-store/ │ ├── docs/ │ ├── architecture.md Boundaries, conventions, risks — read this first -│ └── adr/ 19 decision records +│ └── adr/ 22 decision records │ ├── docker-compose.yml Backing services; `--profile full` runs everything ├── turbo.json pnpm-workspace.yaml package.json @@ -253,11 +253,11 @@ locale-in-path would buy nothing. | **M4** ✅ | Storefront catalog — listings, PDP, variant selector, filters, sort control, load-more pagination and a mobile filter sheet | | **M5** ✅ | Cart (Redis), guest checkout, orders with stock reservation and an admin order lifecycle | | **M6** ✅ | Search: PostgreSQL full-text + trigram, diacritic-folded, ranked, with type-ahead and refinable results | -| **M7** | Promotions, coupons, reviews, CMS | +| **M7** ◐ | Discounts end-to-end: one engine, one admin screen, stacking, windows, limits, redemptions. Reviews and CMS remain | | **M8** | Customer account | | **M9** | Payments (VNPay, MoMo, ZaloPay, COD), shipping, notifications | -**Recommended next step: M7 (promotions, coupons, reviews, CMS) or M9 (payments).** The store can +**Recommended next step: M8 (customer accounts).** The store can now be browsed, searched, filled into a bag and checked out, and every order moves stock through a ledger. What it still cannot do is take money — which is the one gap between this and a shop that trades. @@ -270,9 +270,9 @@ Everything below was run, not assumed: - `pnpm lint` · `pnpm typecheck` · `pnpm test` · `pnpm build` — 27/27 Turborepo tasks pass; `pnpm format:check` clean -- 7 migrations, 35 tables; seed loads 36 permissions, 6 roles, 3 brands, 8 categories, +- 10 migrations, 43 tables; seed loads 36 permissions, 6 roles, 3 brands, 8 categories, 3 collections, 12 products, **155 variants**, 64 uploaded images and 3 dev accounts -- **60 tests** — RBAC guards, password hashing, translation fallback, `Accept-Language`, the +- **78 tests** — RBAC guards, password hashing, translation fallback, `Accept-Language`, the variant matrix planner, the HTTP client's fetch receiver and retry recursion, the inventory list's variant-driven projection, the two admin-schema defects that caused silent data loss, and order-number round-tripping @@ -303,6 +303,24 @@ through Chrome with the console and network panel open: media library upload driven from the browser (presign → PUT to MinIO → register, 400×500 PNG landed at 10,962 bytes with a date-partitioned UUID key); inventory adjustment from the table wrote a ledger entry and the storefront went `OUT_OF_STOCK` → `IN_STOCK` on the next request +- **Discounts (M7):** an automatic promotion and a coupon stack to −150.000 ₫ on a 1.290.000 ₫ + bag; a lowercase code is accepted; a fully-claimed code is refused with a reason and the bag + falls back to the automatic offer; a two-use limit allows exactly two orders; cancelling an + order returns its use; two simultaneous redemptions of the last use produce one discounted + order and one clear refusal; a bogus code reports once and is not remembered +- **The discount admin (M7):** a promotion authored at 09:00 local stores as 02:00Z and reopens + at 09:00, not shifted; a discount scheduled for next week reads "Scheduled" and stays out of + the bag; switching one off in admin drops it from a shopper's bag on the next load; retiring + one soft-deletes it, leaving redemptions and the audit trail intact +- **Content (M7):** a post published in one language still lists on the other locale's journal + rather than vanishing; a body containing `