Stage M5 and Stage M6

This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:23 +07:00
parent 7688657d3c
commit 624a6402bf
77 changed files with 4879 additions and 176 deletions
@@ -0,0 +1,88 @@
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'CONFIRMED', 'FULFILLED', 'COMPLETED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "PaymentStatus" AS ENUM ('UNPAID', 'PAID', 'PARTIALLY_REFUNDED', 'REFUNDED');
-- CreateEnum
CREATE TYPE "FulfillmentStatus" AS ENUM ('UNFULFILLED', 'PARTIALLY_FULFILLED', 'FULFILLED');
-- CreateTable
CREATE TABLE "orders" (
"id" UUID NOT NULL,
"number" SERIAL NOT NULL,
"customer_id" UUID,
"email" VARCHAR(255) NOT NULL,
"phone" VARCHAR(20) NOT NULL,
"status" "OrderStatus" NOT NULL DEFAULT 'PENDING',
"payment_status" "PaymentStatus" NOT NULL DEFAULT 'UNPAID',
"fulfillment_status" "FulfillmentStatus" NOT NULL DEFAULT 'UNFULFILLED',
"currency" "Currency" NOT NULL DEFAULT 'VND',
"subtotal_amount" INTEGER NOT NULL,
"discount_amount" INTEGER NOT NULL DEFAULT 0,
"shipping_amount" INTEGER NOT NULL DEFAULT 0,
"tax_amount" INTEGER NOT NULL DEFAULT 0,
"total_amount" INTEGER NOT NULL,
"ship_full_name" VARCHAR(160) NOT NULL,
"ship_phone" VARCHAR(20) NOT NULL,
"ship_line1" VARCHAR(255) NOT NULL,
"ship_line2" VARCHAR(255),
"ship_ward" VARCHAR(120),
"ship_district" VARCHAR(120),
"ship_province" VARCHAR(120) NOT NULL,
"ship_country_code" CHAR(2) NOT NULL DEFAULT 'VN',
"ship_postal_code" VARCHAR(20),
"customer_note" VARCHAR(1000),
"placed_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"confirmed_at" TIMESTAMPTZ(3),
"cancelled_at" TIMESTAMPTZ(3),
"cancel_reason" VARCHAR(500),
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "orders_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "order_lines" (
"id" UUID NOT NULL,
"order_id" UUID NOT NULL,
"variant_id" UUID,
"product_name" VARCHAR(255) NOT NULL,
"variant_title" VARCHAR(255) NOT NULL,
"sku" VARCHAR(64) NOT NULL,
"image_url" VARCHAR(500),
"unit_amount" INTEGER NOT NULL,
"quantity" INTEGER NOT NULL,
"line_amount" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "order_lines_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "orders_number_key" ON "orders"("number");
-- CreateIndex
CREATE INDEX "orders_customer_id_idx" ON "orders"("customer_id");
-- CreateIndex
CREATE INDEX "orders_email_idx" ON "orders"("email");
-- CreateIndex
CREATE INDEX "orders_status_placed_at_idx" ON "orders"("status", "placed_at");
-- CreateIndex
CREATE INDEX "order_lines_order_id_idx" ON "order_lines"("order_id");
-- CreateIndex
CREATE INDEX "order_lines_variant_id_idx" ON "order_lines"("variant_id");
-- AddForeignKey
ALTER TABLE "orders" ADD CONSTRAINT "orders_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "order_lines" ADD CONSTRAINT "order_lines_order_id_fkey" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "order_lines" ADD CONSTRAINT "order_lines_variant_id_fkey" FOREIGN KEY ("variant_id") REFERENCES "product_variants"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,64 @@
-- Full-text search over the catalog, in PostgreSQL (ADR-0012).
--
-- Two extensions do the work that a dedicated search engine would otherwise be
-- brought in for:
-- unaccent — so "ao chay bo" finds "Áo Chạy Bộ". Vietnamese shoppers type
-- without diacritics constantly; without this, most of them find
-- nothing.
-- pg_trgm — trigram similarity, which gives typo tolerance and partial-word
-- matching that a tsquery alone cannot ("nocturn", "jacket run").
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- `unaccent()` is STABLE, not IMMUTABLE, because it depends on a dictionary that
-- could in principle be changed. Postgres therefore refuses it in a generated
-- column or an index. Pinning the dictionary by name makes the result genuinely
-- immutable, which is the standard way around this.
CREATE OR REPLACE FUNCTION immutable_unaccent(text)
RETURNS text
LANGUAGE sql
IMMUTABLE
STRICT
PARALLEL SAFE
AS $$
SELECT public.unaccent('public.unaccent'::regdictionary, $1)
$$;
-- CreateTable
CREATE TABLE "search_documents" (
"product_id" UUID NOT NULL,
"locale" "Locale" NOT NULL,
"title" VARCHAR(255) NOT NULL,
"keywords" TEXT NOT NULL,
"body" TEXT NOT NULL,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "search_documents_pkey" PRIMARY KEY ("product_id","locale")
);
-- AddForeignKey
ALTER TABLE "search_documents" ADD CONSTRAINT "search_documents_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- The searchable vector is GENERATED, not written by the application.
--
-- That is the whole point: an index maintained by hand drifts from the text it
-- indexes the first time someone updates one and forgets the other. Here it
-- cannot — the database recomputes it on every write to the source columns.
--
-- `simple` rather than `english`: the catalog is bilingual, and English
-- stemming applied to Vietnamese produces nonsense. Weighting carries the
-- relevance instead of stemming.
ALTER TABLE "search_documents"
ADD COLUMN "document" tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('simple', immutable_unaccent(coalesce("title", ''))), 'A') ||
setweight(to_tsvector('simple', immutable_unaccent(coalesce("keywords", ''))), 'B') ||
setweight(to_tsvector('simple', immutable_unaccent(coalesce("body", ''))), 'C')
) STORED;
CREATE INDEX "search_documents_document_idx" ON "search_documents" USING GIN ("document");
-- Trigram index over the high-signal text only. Including `body` would bloat it
-- for matches nobody wants ranked by similarity anyway.
CREATE INDEX "search_documents_trgm_idx" ON "search_documents"
USING GIN ((immutable_unaccent("title") || ' ' || immutable_unaccent("keywords")) gin_trgm_ops);