Files
vmall/apps/api/migrations/0026_threshold_promotions.sql
Chengdong Zhang 9e3abf95ef feat(marketing): add threshold promotions (满减)
Shop-scoped threshold rules with bilingual names, integer
threshold/reduction (reduction < threshold), active windows and display
order. Checkout applies the best eligible rule per shop with currency
conversion; coupon/threshold/activity pricing are mutually exclusive per
shop order. Reduction persists in orders.discount_minor +
threshold_promotion_id and flows into payment totals; checkout
idempotency covers the threshold selection. Delete archives referenced
rules, hard-deletes unreferenced ones.

- migration 0026: threshold_promotions + orders.threshold_promotion_id
  + coupon/threshold exclusivity CHECK
- api: modules/promotion (CRUD, preview, checkout integration)
- shared: ThresholdPromotion types, shop CRUD + preview client methods,
  checkout threshold_by_shop
- shop-admin: promotions page + nav + en/zh locales
- mall: exclusive promotion radio on checkout, live preview wiring,
  threshold label on order detail
- tests: promotions.rs (scoping, validation, best-rule, exclusivity,
  activity-priced 409, totals, replay, archive-vs-delete)
- openspec: archived 2026-10-19-add-threshold-promotions; spec
  threshold-promotions created, order/frontend specs updated
2026-10-19 18:14:38 +08:00

37 lines
1.6 KiB
SQL

-- Threshold promotions (满减): shop-owned automatic reduction rules. A rule is
-- priced at checkout, never claimed, so it carries no stock and cancellation
-- restores nothing.
--
-- `orders` gains the rule reference; the realized amount continues to live in
-- the existing `discount_minor`. An order applies at most one of a coupon or
-- a threshold rule.
CREATE TABLE threshold_promotions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_id UUID NOT NULL REFERENCES shops (id) ON DELETE CASCADE,
name JSONB NOT NULL,
currency CHAR(3) NOT NULL REFERENCES currencies (code),
threshold_minor BIGINT NOT NULL CHECK (threshold_minor > 0),
reduction_minor BIGINT NOT NULL CHECK (reduction_minor > 0),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
archived BOOLEAN NOT NULL DEFAULT FALSE,
display_order INT NOT NULL DEFAULT 0,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT threshold_promotions_window CHECK (ends_at > starts_at),
CONSTRAINT threshold_promotions_amounts CHECK (reduction_minor < threshold_minor)
);
CREATE INDEX threshold_promotions_shop_idx
ON threshold_promotions (shop_id, enabled, archived, starts_at, ends_at);
ALTER TABLE orders
ADD COLUMN threshold_promotion_id UUID REFERENCES threshold_promotions (id) ON DELETE SET NULL;
-- Exactly one price-adjustment source per order.
ALTER TABLE orders
ADD CONSTRAINT orders_one_promotion_source
CHECK (coupon_id IS NULL OR threshold_promotion_id IS NULL);