{returnableContextItems.map((item) => {
const max = orderItemNetQuantity(item);
@@ -1528,10 +1582,28 @@ function WorkflowScreen({
BÀN / PHÒNG
-
{contextTable ? `Cập nhật ${vertical === "karaoke" ? "phòng" : "bàn"} ${contextTable.tableNumber}` : "Chọn bàn/phòng"}
-
Chuyển đơn giữa bàn/phòng sẽ bật khi quy trình chuyển bàn được hoàn thiện. Màn này chỉ cập nhật trạng thái bàn/phòng.
+
{contextOrder ? `Chuyển đơn ${contextOrder.id.slice(0, 8).toUpperCase()}` : contextTable ? `Cập nhật ${vertical === "karaoke" ? "phòng" : "bàn"} ${contextTable.tableNumber}` : "Chọn đơn hoặc bàn/phòng"}
+
{contextOrder ? `Đơn hiện ở ${contextOrder.tableNumber ? `${vertical === "karaoke" ? "phòng" : "bàn"} ${contextOrder.tableNumber}` : "chưa có bàn/phòng"}. Chỉ chuyển sang bàn/phòng đang trống.` : "Truyền orderId để chuyển đơn, hoặc tableId/roomId để cập nhật trạng thái bàn/phòng."}
- {contextTable ? (
+ {contextOrder ? (
+ <>
+
+
+ {!contextOrder.tableId ?
Đơn chưa gắn bàn/phòng nên không thể chuyển.
: null}
+ >
+ ) : contextTable ? (
<>
) : null}
diff --git a/microservices/apps/tpos-mvp-next/src/components/tpos-config.ts b/microservices/apps/tpos-mvp-next/src/components/tpos-config.ts
index e1396dcc..26a257d5 100644
--- a/microservices/apps/tpos-mvp-next/src/components/tpos-config.ts
+++ b/microservices/apps/tpos-mvp-next/src/components/tpos-config.ts
@@ -55,7 +55,7 @@ export const verticals: Array<{ id: VerticalKind; label: string; icon: typeof Co
{ id: "karaoke", label: "Karaoke", icon: DoorOpen },
{ id: "restaurant", label: "Nhà hàng", icon: UtensilsCrossed },
{ id: "cafe", label: "Café", icon: Coffee },
- { id: "spa", label: "Spa", icon: Sparkles },
+ { id: "spa", label: "Spa", icon: Sparkles, visibleInPosNav: false },
{ id: "beauty", label: "Beauty", icon: Heart, visibleInPosNav: false },
{ id: "retail", label: "Bán lẻ", icon: ShoppingBag }
];
diff --git a/microservices/apps/tpos-mvp-next/src/server/db/queries.ts b/microservices/apps/tpos-mvp-next/src/server/db/queries.ts
index d0b4152b..be766590 100644
--- a/microservices/apps/tpos-mvp-next/src/server/db/queries.ts
+++ b/microservices/apps/tpos-mvp-next/src/server/db/queries.ts
@@ -518,7 +518,8 @@ export async function getShop(shopId?: string | null) {
return rows[0] ? mapShop(rows[0]) : null;
}
-export async function getPublicShop(shopId: string) {
+export async function getPublicShop(identifier: string) {
+ const shopId = isUuidIdentifier(identifier) ? identifier : null;
const rows = await query(
`
SELECT s.*, bc.name AS category_name, ss.name AS status_name
@@ -527,14 +528,21 @@ export async function getPublicShop(shopId: string) {
LEFT JOIN shop_statuses ss ON ss.id = s.status_id
WHERE COALESCE(s.is_deleted, false) = false
AND s.status_id = 2
- AND s.id = $1::uuid
+ AND (
+ ($1::uuid IS NOT NULL AND s.id = $1::uuid)
+ OR s.slug = $2
+ )
LIMIT 1
`,
- [shopId]
+ [shopId, identifier]
);
return rows[0] ? mapShop(rows[0]) : null;
}
+function isUuidIdentifier(value: string) {
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
+}
+
export async function updateShop(
shopId: string,
input: {
@@ -1494,7 +1502,7 @@ export async function getTableByToken(token: string) {
WHERE t.qr_token = $1
AND COALESCE(s.is_deleted, false) = false
AND s.status_id = 2
- AND t.status_id <> 4
+ AND t.status_id IN (1, 2)
`,
[token]
);
@@ -1826,6 +1834,113 @@ export async function getOrderById(orderId: string, shopId?: string | null) {
return rows[0] ? mapOrder(rows[0]) : null;
}
+export async function transferOrderTable(orderId: string, input: { shopId?: string | null; targetTableId?: string | null; reason?: string | null }) {
+ await withTransaction(async (client) => {
+ const order = await client.query<{ id: string; shop_id: string; table_id: string | null; status_id: number }>(
+ `
+ SELECT id, shop_id, table_id, status_id
+ FROM orders
+ WHERE id = $1 AND ($2::uuid IS NULL OR shop_id = $2::uuid)
+ FOR UPDATE
+ `,
+ [orderId, input.shopId || null]
+ );
+ const current = order.rows[0];
+ if (!current) throw new Error("Order not found");
+ const currentStatusId = int(current.status_id);
+ if (![1, 2, 4, 7].includes(currentStatusId)) throw new Error("Only open unpaid orders can be transferred between tables");
+ if (!current.table_id) throw new Error("Order is not assigned to a table or room");
+
+ const targetTableId = input.targetTableId?.trim();
+ if (!targetTableId) throw new Error("targetTableId is required");
+ if (current.table_id === targetTableId) return;
+
+ const target = await client.query<{ id: string; status_id: number }>(
+ `
+ SELECT id, status_id
+ FROM tables
+ WHERE id = $1::uuid
+ AND shop_id = $2::uuid
+ FOR UPDATE
+ `,
+ [targetTableId, current.shop_id]
+ );
+ if (!target.rows[0]) throw new Error("Target table does not belong to this shop");
+ if (int(target.rows[0].status_id) !== 1) throw new Error("Target table must be available before transfer");
+
+ const sourceTableId = current.table_id;
+ await client.query(`SELECT id FROM tables WHERE id = $1::uuid FOR UPDATE`, [sourceTableId]);
+
+ await client.query(
+ `UPDATE orders
+ SET table_id = $2::uuid,
+ notes = CASE
+ WHEN $3::text IS NULL THEN notes
+ WHEN notes IS NULL OR trim(notes) = '' THEN $3::text
+ ELSE concat(notes, E'\n', $3::text)
+ END,
+ updated_at = now()
+ WHERE id = $1::uuid`,
+ [
+ orderId,
+ targetTableId,
+ input.reason?.trim() ? `Chuyển bàn/phòng: ${input.reason.trim()}` : null
+ ]
+ );
+
+ if (sourceTableId) {
+ const sourceActiveOrders = await client.query(
+ `SELECT 1
+ FROM orders
+ WHERE table_id = $1::uuid
+ AND id <> $2::uuid
+ AND status_id IN (1, 2, 4, 7)
+ LIMIT 1`,
+ [sourceTableId, orderId]
+ );
+ if (!sourceActiveOrders.rows[0]) {
+ await client.query(`UPDATE tables SET status_id = 1, updated_at = now() WHERE id = $1::uuid`, [sourceTableId]);
+ await client.query(
+ `UPDATE table_sessions
+ SET status = 'closed',
+ closed_at = now()
+ WHERE table_id = $1::uuid
+ AND lower(status) = 'open'`,
+ [sourceTableId]
+ );
+ }
+ }
+
+ await client.query(
+ `UPDATE table_sessions
+ SET status = 'closed',
+ closed_at = now()
+ WHERE table_id = $1::uuid
+ AND lower(status) = 'open'`,
+ [targetTableId]
+ );
+ await client.query(`UPDATE tables SET status_id = 2, updated_at = now() WHERE id = $1::uuid`, [targetTableId]);
+ await client.query(
+ `INSERT INTO table_sessions (id, shop_id, table_id, status, guest_count)
+ SELECT $1, $2::uuid, $3::uuid, 'open', 1
+ WHERE NOT EXISTS (
+ SELECT 1 FROM table_sessions
+ WHERE table_id = $3::uuid
+ AND lower(status) = 'open'
+ )`,
+ [randomUUID(), current.shop_id, targetTableId]
+ );
+
+ await logActivity(client, "order.table_transferred", "order", orderId, current.shop_id, {
+ fromTableId: sourceTableId,
+ toTableId: targetTableId,
+ reason: input.reason ?? null
+ });
+ });
+
+ return getOrderById(orderId, input.shopId ?? null);
+}
+
export async function cancelOrder(orderId: string, shopId?: string | null, reason?: string | null) {
return withTransaction(async (client) => {
const refreshedOrder = async () => {
diff --git a/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-1.ts b/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-1.ts
new file mode 100644
index 00000000..d37b1d45
--- /dev/null
+++ b/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-1.ts
@@ -0,0 +1,390 @@
+export const coreSchemaSqlPart1 = String.raw`
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+ CREATE TABLE IF NOT EXISTS merchant_types (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS merchant_statuses (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS verification_statuses (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS shop_types (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS shop_statuses (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS business_categories (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS product_types (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS order_statuses (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS item_types (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS transaction_types (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS table_statuses (
+ id integer PRIMARY KEY,
+ name varchar(50) NOT NULL
+ );
+
+ INSERT INTO merchant_types (id, name) VALUES (1, 'Individual'), (2, 'Company')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO merchant_statuses (id, name) VALUES (1, 'PendingApproval'), (2, 'Active'), (3, 'Suspended'), (4, 'Banned')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO verification_statuses (id, name) VALUES (1, 'Unverified'), (2, 'Pending'), (3, 'Verified'), (4, 'Rejected')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO shop_types (id, name) VALUES (1, 'OnlineOnly'), (2, 'PhysicalOnly'), (3, 'Hybrid')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO shop_statuses (id, name) VALUES (1, 'Draft'), (2, 'Active'), (3, 'Inactive'), (4, 'Closed')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO business_categories (id, name) VALUES
+ (1, 'FoodBeverage'), (2, 'Fashion'), (3, 'Electronics'), (4, 'Healthcare'),
+ (5, 'Beauty'), (6, 'Education'), (7, 'Entertainment'), (8, 'Services'),
+ (9, 'Grocery'), (10, 'HomeFurniture'), (11, 'Cafe'), (12, 'Restaurant'),
+ (13, 'Karaoke'), (14, 'Spa'), (99, 'Other')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO product_types (id, name) VALUES (1, 'Physical'), (2, 'Service'), (3, 'PreparedFood')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO order_statuses (id, name) VALUES
+ (1, 'Draft'), (2, 'Validated'), (3, 'Paid'), (4, 'Processing'),
+ (5, 'Completed'), (6, 'Cancelled'), (7, 'PaymentPending'), (8, 'Returned')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO item_types (id, name) VALUES (1, 'RawMaterial'), (2, 'FinishedGood'), (3, 'Consumable')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO transaction_types (id, name) VALUES (1, 'In'), (2, 'Out'), (3, 'Adjustment'), (4, 'Reserve'), (5, 'Release')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+ INSERT INTO table_statuses (id, name) VALUES (1, 'Available'), (2, 'Occupied'), (3, 'Reserved'), (4, 'Cleaning')
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
+
+ CREATE TABLE IF NOT EXISTS merchants (
+ id uuid PRIMARY KEY,
+ user_id uuid NOT NULL,
+ business_name varchar(200) NOT NULL,
+ type_id integer NOT NULL DEFAULT 1,
+ status_id integer NOT NULL DEFAULT 2,
+ verification_status_id integer NOT NULL DEFAULT 1,
+ subscription_plan_id integer NOT NULL DEFAULT 0,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz,
+ is_deleted boolean NOT NULL DEFAULT false
+ );
+ ALTER TABLE merchants ADD COLUMN IF NOT EXISTS subscription_plan_id integer NOT NULL DEFAULT 0;
+ ALTER TABLE merchants ADD COLUMN IF NOT EXISTS is_deleted boolean NOT NULL DEFAULT false;
+
+ CREATE TABLE IF NOT EXISTS shops (
+ id uuid PRIMARY KEY,
+ merchant_id uuid NOT NULL,
+ name varchar(100) NOT NULL,
+ slug varchar(100) NOT NULL,
+ type_id integer NOT NULL DEFAULT 2,
+ category_id integer NOT NULL DEFAULT 9,
+ status_id integer NOT NULL DEFAULT 2,
+ description varchar(2000),
+ phone varchar(20),
+ email varchar(100),
+ website varchar(200),
+ logo_url varchar(500),
+ cover_image_url varchar(500),
+ features_config jsonb,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz,
+ is_default boolean NOT NULL DEFAULT false,
+ is_deleted boolean NOT NULL DEFAULT false
+ );
+ ALTER TABLE shops ADD COLUMN IF NOT EXISTS phone varchar(20);
+ ALTER TABLE shops ADD COLUMN IF NOT EXISTS email varchar(100);
+ ALTER TABLE shops ADD COLUMN IF NOT EXISTS website varchar(200);
+ ALTER TABLE shops ADD COLUMN IF NOT EXISTS features_config jsonb;
+ ALTER TABLE shops ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false;
+ ALTER TABLE shops ADD COLUMN IF NOT EXISTS is_deleted boolean NOT NULL DEFAULT false;
+
+ CREATE TABLE IF NOT EXISTS categories (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ name varchar(200) NOT NULL,
+ description varchar(1000),
+ parent_id uuid,
+ display_order integer NOT NULL DEFAULT 0,
+ image_url varchar(500),
+ is_active boolean NOT NULL DEFAULT true,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+ ALTER TABLE categories ADD COLUMN IF NOT EXISTS image_url varchar(500);
+ ALTER TABLE categories ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
+
+ CREATE TABLE IF NOT EXISTS products (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ name varchar(255) NOT NULL,
+ description varchar(2000),
+ price numeric(18,2) NOT NULL,
+ type_id integer NOT NULL DEFAULT 1,
+ attributes jsonb,
+ image_url varchar(500),
+ sku varchar(100),
+ barcode varchar(100),
+ category_id uuid,
+ is_active boolean NOT NULL DEFAULT true,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+ ALTER TABLE products ADD COLUMN IF NOT EXISTS barcode varchar(100);
+ ALTER TABLE products ADD COLUMN IF NOT EXISTS category_id uuid;
+ ALTER TABLE products ADD COLUMN IF NOT EXISTS image_url varchar(500);
+ ALTER TABLE products ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
+
+ CREATE TABLE IF NOT EXISTS inventory_items (
+ id uuid PRIMARY KEY,
+ product_id uuid NOT NULL,
+ shop_id uuid NOT NULL,
+ name varchar(200),
+ item_type_id integer NOT NULL DEFAULT 2,
+ unit varchar(20) NOT NULL DEFAULT 'pcs',
+ cost_per_unit numeric(18,4) NOT NULL DEFAULT 0,
+ supplier_name varchar(200),
+ expiry_date timestamptz,
+ quantity integer NOT NULL DEFAULT 0,
+ reserved_quantity integer NOT NULL DEFAULT 0,
+ reorder_level integer NOT NULL DEFAULT 10,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz,
+ deleted_at timestamptz
+ );
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS name varchar(200);
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS item_type_id integer NOT NULL DEFAULT 2;
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS unit varchar(20) NOT NULL DEFAULT 'pcs';
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS cost_per_unit numeric(18,4) NOT NULL DEFAULT 0;
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS supplier_name varchar(200);
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS expiry_date timestamptz;
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
+ ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS deleted_at timestamptz;
+
+ CREATE TABLE IF NOT EXISTS inventory_transactions (
+ id uuid PRIMARY KEY,
+ inventory_item_id uuid NOT NULL,
+ type_id integer NOT NULL,
+ quantity integer NOT NULL,
+ reference_id uuid,
+ notes varchar(500),
+ invoice_image_url varchar(1000),
+ unit_cost numeric(18,4),
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+ ALTER TABLE inventory_transactions ADD COLUMN IF NOT EXISTS invoice_image_url varchar(1000);
+ ALTER TABLE inventory_transactions ADD COLUMN IF NOT EXISTS unit_cost numeric(18,4);
+
+ CREATE TABLE IF NOT EXISTS tables (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ table_number varchar(20) NOT NULL,
+ capacity integer NOT NULL DEFAULT 2,
+ zone varchar(100),
+ status_id integer NOT NULL DEFAULT 1,
+ position_x integer,
+ position_y integer,
+ qr_token varchar(64),
+ hourly_rate numeric(18,2) NOT NULL DEFAULT 0,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS orders (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ customer_id uuid,
+ table_id uuid,
+ status_id integer NOT NULL DEFAULT 1,
+ total_amount numeric(18,2) NOT NULL DEFAULT 0,
+ notes varchar(2000),
+ discount_amount numeric(18,2) NOT NULL DEFAULT 0,
+ discount_type varchar(50),
+ discount_reference varchar(255),
+ payment_method varchar(50),
+ transaction_id varchar(255),
+ amount_tendered numeric(18,2),
+ change_amount numeric(18,2),
+ return_reason varchar(1000),
+ returned_at timestamptz,
+ is_return boolean NOT NULL DEFAULT false,
+ original_order_id uuid,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS table_id uuid;
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_method varchar(50);
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS transaction_id varchar(255);
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS amount_tendered numeric(18,2);
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS change_amount numeric(18,2);
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS discount_amount numeric(18,2) NOT NULL DEFAULT 0;
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS discount_type varchar(50);
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS discount_reference varchar(255);
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS return_reason varchar(1000);
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS returned_at timestamptz;
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS is_return boolean NOT NULL DEFAULT false;
+ ALTER TABLE orders ADD COLUMN IF NOT EXISTS original_order_id uuid;
+
+ CREATE TABLE IF NOT EXISTS order_items (
+ id uuid PRIMARY KEY,
+ order_id uuid NOT NULL,
+ product_id uuid NOT NULL,
+ product_name varchar(255) NOT NULL,
+ product_type varchar(50) NOT NULL,
+ quantity integer NOT NULL,
+ unit_price numeric(18,2) NOT NULL,
+ status varchar(50) NOT NULL DEFAULT 'Completed',
+ track_inventory boolean NOT NULL DEFAULT true,
+ metadata jsonb
+ );
+ ALTER TABLE order_items ADD COLUMN IF NOT EXISTS track_inventory boolean NOT NULL DEFAULT true;
+
+ CREATE TABLE IF NOT EXISTS payment_transactions (
+ id uuid PRIMARY KEY,
+ order_id uuid NOT NULL,
+ shop_id uuid NOT NULL,
+ method varchar(50) NOT NULL,
+ amount numeric(18,2) NOT NULL,
+ amount_tendered numeric(18,2),
+ change_amount numeric(18,2) NOT NULL DEFAULT 0,
+ status varchar(50) NOT NULL DEFAULT 'Succeeded',
+ provider_reference varchar(255),
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS order_returns (
+ id uuid PRIMARY KEY,
+ order_id uuid NOT NULL,
+ shop_id uuid NOT NULL,
+ return_type varchar(50) NOT NULL DEFAULT 'return',
+ reason varchar(1000),
+ refund_amount numeric(18,2) NOT NULL DEFAULT 0,
+ status varchar(50) NOT NULL DEFAULT 'completed',
+ idempotency_key varchar(128),
+ request_hash char(64),
+ created_by uuid,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS return_type varchar(50) NOT NULL DEFAULT 'return';
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS reason varchar(1000);
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS refund_amount numeric(18,2) NOT NULL DEFAULT 0;
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS status varchar(50) NOT NULL DEFAULT 'completed';
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS idempotency_key varchar(128);
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS request_hash char(64);
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS created_by uuid;
+ ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS updated_at timestamptz;
+
+ CREATE TABLE IF NOT EXISTS order_return_items (
+ id uuid PRIMARY KEY,
+ return_id uuid NOT NULL,
+ order_item_id uuid NOT NULL,
+ product_id uuid NOT NULL,
+ product_name varchar(255) NOT NULL,
+ quantity integer NOT NULL,
+ unit_price numeric(18,2) NOT NULL,
+ restock boolean NOT NULL DEFAULT false,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+ ALTER TABLE order_return_items ADD COLUMN IF NOT EXISTS restock boolean NOT NULL DEFAULT false;
+
+ CREATE TABLE IF NOT EXISTS refund_transactions (
+ id uuid PRIMARY KEY,
+ return_id uuid,
+ order_id uuid NOT NULL,
+ shop_id uuid NOT NULL,
+ method varchar(50) NOT NULL DEFAULT 'cash',
+ amount numeric(18,2) NOT NULL,
+ status varchar(50) NOT NULL DEFAULT 'Succeeded',
+ reason varchar(1000),
+ provider_reference varchar(255),
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+ ALTER TABLE refund_transactions ADD COLUMN IF NOT EXISTS return_id uuid;
+ ALTER TABLE refund_transactions ADD COLUMN IF NOT EXISTS reason varchar(1000);
+ ALTER TABLE refund_transactions ADD COLUMN IF NOT EXISTS provider_reference varchar(255);
+
+ CREATE TABLE IF NOT EXISTS mvp_roles (
+ id uuid PRIMARY KEY,
+ code varchar(50) NOT NULL UNIQUE,
+ name varchar(100) NOT NULL,
+ portal varchar(50) NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS mvp_users (
+ id uuid PRIMARY KEY,
+ email varchar(200) NOT NULL UNIQUE,
+ password_hash varchar(255) NOT NULL,
+ display_name varchar(200) NOT NULL,
+ phone varchar(50),
+ status varchar(50) NOT NULL DEFAULT 'active',
+ default_shop_id uuid,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS mvp_user_roles (
+ user_id uuid NOT NULL,
+ role_id uuid NOT NULL,
+ shop_id uuid
+ );
+
+ CREATE TABLE IF NOT EXISTS mvp_sessions (
+ id uuid PRIMARY KEY,
+ user_id uuid NOT NULL,
+ token_hash varchar(255) NOT NULL UNIQUE,
+ expires_at timestamptz NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS staff_members (
+ id uuid PRIMARY KEY,
+ user_id uuid,
+ shop_id uuid,
+ employee_code varchar(50),
+ first_name varchar(100),
+ last_name varchar(100),
+ phone varchar(50),
+ email varchar(200),
+ role varchar(80),
+ status varchar(50) NOT NULL DEFAULT 'active',
+ joined_at timestamptz NOT NULL DEFAULT now(),
+ terminated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS staff_schedules (
+ id uuid PRIMARY KEY,
+ staff_id uuid NOT NULL,
+ shop_id uuid NOT NULL,
+ day_of_week integer NOT NULL,
+ start_time varchar(20) NOT NULL,
+ end_time varchar(20) NOT NULL,`;
diff --git a/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-2.ts b/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-2.ts
new file mode 100644
index 00000000..df14e4a4
--- /dev/null
+++ b/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-2.ts
@@ -0,0 +1,390 @@
+export const coreSchemaSqlPart2 = String.raw` created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS attendance_records (
+ id uuid PRIMARY KEY,
+ staff_id uuid NOT NULL,
+ shop_id uuid,
+ check_in_at timestamptz NOT NULL DEFAULT now(),
+ check_out_at timestamptz,
+ status varchar(50) NOT NULL DEFAULT 'checked_in',
+ note varchar(500)
+ );
+
+ CREATE TABLE IF NOT EXISTS leave_requests (
+ id uuid PRIMARY KEY,
+ staff_id uuid NOT NULL,
+ shop_id uuid,
+ from_date date NOT NULL,
+ to_date date NOT NULL,
+ reason varchar(500),
+ status varchar(50) NOT NULL DEFAULT 'pending',
+ reviewed_by uuid,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS notifications (
+ id uuid PRIMARY KEY,
+ user_id uuid,
+ shop_id uuid,
+ title varchar(200) NOT NULL,
+ body varchar(1000),
+ status varchar(50) NOT NULL DEFAULT 'unread',
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS members (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ display_name varchar(200),
+ phone varchar(50),
+ gender varchar(50),
+ country_code varchar(10),
+ current_exp integer NOT NULL DEFAULT 0,
+ current_level integer NOT NULL DEFAULT 1,
+ total_exp_earned integer NOT NULL DEFAULT 0,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS membership_levels (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ level_number integer NOT NULL,
+ name varchar(100) NOT NULL,
+ required_exp integer NOT NULL DEFAULT 0,
+ description varchar(500),
+ badge_color varchar(30),
+ is_active boolean NOT NULL DEFAULT true
+ );
+
+ CREATE TABLE IF NOT EXISTS experience_transactions (
+ id uuid PRIMARY KEY,
+ member_id uuid NOT NULL,
+ points integer NOT NULL,
+ source varchar(80) NOT NULL,
+ reference_id varchar(100),
+ level_at_time integer NOT NULL DEFAULT 1,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS wallets (
+ id uuid PRIMARY KEY,
+ owner_id uuid NOT NULL,
+ currency varchar(10) NOT NULL DEFAULT 'VND',
+ balance numeric(18,2) NOT NULL DEFAULT 0,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS wallet_transactions (
+ id uuid PRIMARY KEY,
+ wallet_id uuid NOT NULL,
+ amount numeric(18,2) NOT NULL,
+ description varchar(500),
+ item_name varchar(200),
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS campaigns (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ name varchar(200) NOT NULL,
+ description varchar(1000),
+ face_value numeric(18,2) NOT NULL DEFAULT 0,
+ discount_type varchar(50) NOT NULL DEFAULT 'fixed',
+ discount_value numeric(18,2) NOT NULL DEFAULT 0,
+ total_vouchers integer NOT NULL DEFAULT 0,
+ issued_vouchers integer NOT NULL DEFAULT 0,
+ status varchar(50) NOT NULL DEFAULT 'draft',
+ start_date timestamptz,
+ end_date timestamptz,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS vouchers (
+ id uuid PRIMARY KEY,
+ campaign_id uuid,
+ shop_id uuid,
+ code varchar(100) NOT NULL UNIQUE,
+ status varchar(50) NOT NULL DEFAULT 'active',
+ discount_type varchar(50) NOT NULL DEFAULT 'fixed',
+ discount_value numeric(18,2) NOT NULL DEFAULT 0,
+ redeemed_order_id uuid,
+ redeemed_at timestamptz,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS resources (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ name varchar(200) NOT NULL,
+ resource_type varchar(80),
+ capacity integer NOT NULL DEFAULT 1,
+ is_active boolean NOT NULL DEFAULT true,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS therapists (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ staff_id uuid,
+ name varchar(200) NOT NULL,
+ specialty varchar(200),
+ status varchar(50) NOT NULL DEFAULT 'active',
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS appointments (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ customer_id uuid,
+ staff_id uuid,
+ resource_id uuid,
+ service_id uuid,
+ customer_name varchar(200),
+ service_name varchar(200),
+ therapist_name varchar(200),
+ resource_name varchar(200),
+ start_time timestamptz NOT NULL,
+ end_time timestamptz NOT NULL,
+ status varchar(50) NOT NULL DEFAULT 'pending',
+ notes varchar(1000),
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS recipes (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ product_id uuid,
+ name varchar(200) NOT NULL,
+ ingredients jsonb NOT NULL DEFAULT '[]'::jsonb,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS kitchen_tickets (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ order_id uuid,
+ table_id uuid,
+ table_label varchar(100),
+ status varchar(50) NOT NULL DEFAULT 'Pending',
+ priority varchar(50) NOT NULL DEFAULT 'normal',
+ items jsonb NOT NULL DEFAULT '[]'::jsonb,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS barista_queue (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ order_id uuid,
+ product_name varchar(200) NOT NULL,
+ customer_name varchar(200),
+ status varchar(50) NOT NULL DEFAULT 'Pending',
+ barista_name varchar(200),
+ created_at timestamptz NOT NULL DEFAULT now(),
+ started_at timestamptz,
+ ready_at timestamptz,
+ delivered_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS reservations (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ table_id uuid,
+ customer_name varchar(200) NOT NULL,
+ phone varchar(50),
+ guest_count integer NOT NULL DEFAULT 1,
+ reservation_time timestamptz NOT NULL,
+ status varchar(50) NOT NULL DEFAULT 'pending',
+ notes varchar(1000),
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS table_sessions (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ table_id uuid NOT NULL,
+ status varchar(50) NOT NULL DEFAULT 'open',
+ guest_count integer NOT NULL DEFAULT 1,
+ started_at timestamptz NOT NULL DEFAULT now(),
+ closed_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS storage_folders (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ parent_id uuid,
+ name varchar(200) NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS storage_files (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ folder_id uuid,
+ file_name varchar(255) NOT NULL,
+ content_type varchar(120),
+ byte_size bigint NOT NULL DEFAULT 0,
+ object_key varchar(500) NOT NULL,
+ access_level varchar(50) NOT NULL DEFAULT 'public',
+ public_url varchar(1000),
+ provider varchar(50) NOT NULL DEFAULT 's3',
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS ai_configs (
+ shop_id uuid PRIMARY KEY,
+ provider varchar(50) NOT NULL,
+ api_key_ref varchar(255),
+ model varchar(100) NOT NULL,
+ base_url varchar(500),
+ system_prompt text,
+ enabled boolean NOT NULL DEFAULT true,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS ai_messages (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ user_id uuid,
+ role varchar(50) NOT NULL,
+ content text NOT NULL,
+ tools_used jsonb,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS platform_plans (
+ id uuid PRIMARY KEY,
+ code varchar(50) NOT NULL UNIQUE,
+ name varchar(100) NOT NULL,
+ price numeric(18,2) NOT NULL DEFAULT 0,
+ features jsonb NOT NULL DEFAULT '[]'::jsonb,
+ is_active boolean NOT NULL DEFAULT true
+ );
+
+ CREATE TABLE IF NOT EXISTS feature_flags (
+ key varchar(100) PRIMARY KEY,
+ description varchar(500),
+ enabled boolean NOT NULL DEFAULT false,
+ updated_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS audit_logs (
+ id uuid PRIMARY KEY,
+ actor_user_id uuid,
+ action varchar(150) NOT NULL,
+ entity_type varchar(100),
+ entity_id varchar(100),
+ metadata jsonb,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS social_connections (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ provider varchar(50) NOT NULL,
+ account_name varchar(200),
+ external_id varchar(200),
+ status varchar(50) NOT NULL DEFAULT 'configured',
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+
+ CREATE TABLE IF NOT EXISTS social_posts (
+ id uuid PRIMARY KEY,
+ shop_id uuid,
+ provider varchar(50) NOT NULL,
+ content text NOT NULL,
+ status varchar(50) NOT NULL DEFAULT 'draft',
+ external_id varchar(200),
+ scheduled_at timestamptz,
+ published_at timestamptz,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE TABLE IF NOT EXISTS receipt_templates (
+ id uuid PRIMARY KEY,
+ shop_id uuid NOT NULL,
+ code varchar(80) NOT NULL,
+ name varchar(200) NOT NULL,
+ template_type varchar(50) NOT NULL DEFAULT 'sales_receipt',
+ paper_size varchar(50) NOT NULL DEFAULT '80mm',
+ header_text varchar(500),
+ footer_text varchar(1000),
+ tax_label varchar(120),
+ tax_rate numeric(8,4) NOT NULL DEFAULT 0,
+ show_logo boolean NOT NULL DEFAULT true,
+ show_qr boolean NOT NULL DEFAULT true,
+ show_tax boolean NOT NULL DEFAULT false,
+ kitchen_copy boolean NOT NULL DEFAULT false,
+ is_default boolean NOT NULL DEFAULT false,
+ is_active boolean NOT NULL DEFAULT true,
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz
+ );
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS template_type varchar(50) NOT NULL DEFAULT 'sales_receipt';
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS paper_size varchar(50) NOT NULL DEFAULT '80mm';
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS header_text varchar(500);
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS footer_text varchar(1000);
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS tax_label varchar(120);
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS tax_rate numeric(8,4) NOT NULL DEFAULT 0;
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS show_logo boolean NOT NULL DEFAULT true;
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS show_qr boolean NOT NULL DEFAULT true;
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS show_tax boolean NOT NULL DEFAULT false;
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS kitchen_copy boolean NOT NULL DEFAULT false;
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false;
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
+ ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb;
+
+ CREATE TABLE IF NOT EXISTS mvp_activity (
+ id uuid PRIMARY KEY,
+ action varchar(100) NOT NULL,
+ entity_type varchar(100) NOT NULL,
+ entity_id uuid,
+ shop_id uuid,
+ payload jsonb,
+ created_at timestamptz NOT NULL DEFAULT now()
+ );
+
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_shops_slug ON shops(slug);
+ CREATE INDEX IF NOT EXISTS ix_shops_merchant_id ON shops(merchant_id);
+ CREATE INDEX IF NOT EXISTS ix_categories_shop_id ON categories(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_products_shop_id ON products(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_products_barcode ON products(barcode);
+ CREATE INDEX IF NOT EXISTS ix_inventory_shop_id ON inventory_items(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_inventory_product_id ON inventory_items(product_id);
+ CREATE INDEX IF NOT EXISTS ix_orders_shop_id ON orders(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_orders_created_at ON orders(created_at);
+ CREATE INDEX IF NOT EXISTS ix_order_items_order_id ON order_items(order_id);
+ CREATE INDEX IF NOT EXISTS ix_payment_transactions_order_id ON payment_transactions(order_id);
+ CREATE INDEX IF NOT EXISTS ix_payment_transactions_shop_id ON payment_transactions(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_order_returns_order_id ON order_returns(order_id);
+ CREATE INDEX IF NOT EXISTS ix_order_returns_shop_id ON order_returns(shop_id);
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_order_returns_shop_idempotency_key
+ ON order_returns(shop_id, idempotency_key)
+ WHERE idempotency_key IS NOT NULL;
+ CREATE INDEX IF NOT EXISTS ix_order_return_items_return_id ON order_return_items(return_id);
+ CREATE INDEX IF NOT EXISTS ix_order_return_items_order_item_id ON order_return_items(order_item_id);
+ CREATE INDEX IF NOT EXISTS ix_refund_transactions_order_id ON refund_transactions(order_id);
+ CREATE INDEX IF NOT EXISTS ix_refund_transactions_shop_id ON refund_transactions(shop_id);
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_tables_shop_table_number ON tables(shop_id, table_number);
+ CREATE INDEX IF NOT EXISTS ix_mvp_sessions_token_hash ON mvp_sessions(token_hash);
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_mvp_user_roles_scope ON mvp_user_roles(user_id, role_id, COALESCE(shop_id, '00000000-0000-0000-0000-000000000000'::uuid));
+
+ WITH duplicate_staff_codes AS (
+ SELECT
+ id,
+ employee_code,
+ row_number() OVER (PARTITION BY shop_id, employee_code ORDER BY joined_at NULLS LAST, id) AS duplicate_rank
+ FROM staff_members
+ WHERE employee_code IS NOT NULL AND btrim(employee_code) <> ''
+ )`;
diff --git a/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-3.ts b/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-3.ts
new file mode 100644
index 00000000..09b02e49
--- /dev/null
+++ b/microservices/apps/tpos-mvp-next/src/server/db/schema-sql-part-3.ts
@@ -0,0 +1,376 @@
+export const coreSchemaSqlPart3 = String.raw` UPDATE staff_members staff
+ SET employee_code = left(duplicate_staff_codes.employee_code, 43) || '-' || substr(staff.id::text, 1, 6)
+ FROM duplicate_staff_codes
+ WHERE staff.id = duplicate_staff_codes.id
+ AND duplicate_staff_codes.duplicate_rank > 1;
+
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_staff_members_shop_employee_code ON staff_members(shop_id, employee_code);
+ CREATE INDEX IF NOT EXISTS ix_staff_members_shop_id ON staff_members(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_attendance_staff_id ON attendance_records(staff_id);
+ CREATE INDEX IF NOT EXISTS ix_members_shop_id ON members(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_campaigns_shop_id ON campaigns(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_vouchers_code ON vouchers(code);
+ CREATE INDEX IF NOT EXISTS ix_appointments_shop_id ON appointments(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_kitchen_tickets_shop_id ON kitchen_tickets(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_barista_queue_shop_id ON barista_queue(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_reservations_shop_id ON reservations(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_storage_files_shop_id ON storage_files(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_ai_messages_shop_id ON ai_messages(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_receipt_templates_shop_id ON receipt_templates(shop_id);
+ CREATE INDEX IF NOT EXISTS ix_audit_logs_created_at ON audit_logs(created_at DESC);
+ CREATE INDEX IF NOT EXISTS ix_mvp_activity_created_at ON mvp_activity(created_at DESC);
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_receipt_templates_shop_code ON receipt_templates(shop_id, code);
+
+ DO $integrity_indexes$
+ BEGIN
+ IF to_regclass('public.ux_tables_qr_token_not_null') IS NULL THEN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM tables
+ WHERE qr_token IS NOT NULL
+ GROUP BY qr_token
+ HAVING COUNT(*) > 1
+ ) THEN
+ CREATE UNIQUE INDEX ux_tables_qr_token_not_null ON tables(qr_token) WHERE qr_token IS NOT NULL;
+ ELSE
+ RAISE NOTICE 'Skipping ux_tables_qr_token_not_null because duplicate qr_token values exist.';
+ END IF;
+ END IF;
+
+ IF to_regclass('public.ux_wallets_owner_currency') IS NULL THEN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM wallets
+ GROUP BY owner_id, currency
+ HAVING COUNT(*) > 1
+ ) THEN
+ CREATE UNIQUE INDEX ux_wallets_owner_currency ON wallets(owner_id, currency);
+ ELSE
+ RAISE NOTICE 'Skipping ux_wallets_owner_currency because duplicate owner/currency wallets exist.';
+ END IF;
+ END IF;
+
+ IF to_regclass('public.ux_payment_transactions_success_order') IS NULL THEN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM payment_transactions
+ WHERE status = 'Succeeded'
+ GROUP BY order_id
+ HAVING COUNT(*) > 1
+ ) THEN
+ CREATE UNIQUE INDEX ux_payment_transactions_success_order
+ ON payment_transactions(order_id)
+ WHERE status = 'Succeeded';
+ ELSE
+ RAISE NOTICE 'Skipping ux_payment_transactions_success_order because duplicate successful payments exist.';
+ END IF;
+ END IF;
+
+ IF to_regclass('public.ux_payment_transactions_terminal_order') IS NULL THEN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM payment_transactions
+ WHERE lower(status) IN ('succeeded', 'completed')
+ GROUP BY order_id
+ HAVING COUNT(*) > 1
+ ) THEN
+ CREATE UNIQUE INDEX ux_payment_transactions_terminal_order
+ ON payment_transactions(order_id)
+ WHERE lower(status) IN ('succeeded', 'completed');
+ ELSE
+ RAISE NOTICE 'Skipping ux_payment_transactions_terminal_order because duplicate terminal payments exist.';
+ END IF;
+ END IF;
+
+ IF to_regclass('public.ux_table_sessions_one_open') IS NULL THEN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM table_sessions
+ WHERE lower(status) = 'open'
+ GROUP BY table_id
+ HAVING COUNT(*) > 1
+ ) THEN
+ CREATE UNIQUE INDEX ux_table_sessions_one_open
+ ON table_sessions(table_id)
+ WHERE lower(status) = 'open';
+ ELSE
+ RAISE NOTICE 'Skipping ux_table_sessions_one_open because duplicate open table sessions exist.';
+ END IF;
+ END IF;
+
+ DROP INDEX IF EXISTS ux_inventory_items_shop_product;
+ IF to_regclass('public.ux_inventory_items_shop_product') IS NULL THEN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM inventory_items
+ WHERE product_id IS NOT NULL AND deleted_at IS NULL
+ GROUP BY shop_id, product_id
+ HAVING COUNT(*) > 1
+ ) THEN
+ CREATE UNIQUE INDEX ux_inventory_items_shop_product
+ ON inventory_items(shop_id, product_id)
+ WHERE product_id IS NOT NULL AND deleted_at IS NULL;
+ ELSE
+ RAISE NOTICE 'Skipping ux_inventory_items_shop_product because duplicate shop/product inventory rows exist.';
+ END IF;
+ END IF;
+ END
+ $integrity_indexes$;
+
+ DO $integrity_constraints$
+ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_staff_members_user') THEN
+ ALTER TABLE staff_members
+ ADD CONSTRAINT fk_staff_members_user FOREIGN KEY (user_id) REFERENCES mvp_users(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_staff_members_shop') THEN
+ ALTER TABLE staff_members
+ ADD CONSTRAINT fk_staff_members_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_wallets_owner') THEN
+ ALTER TABLE wallets
+ ADD CONSTRAINT fk_wallets_owner FOREIGN KEY (owner_id) REFERENCES mvp_users(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_wallet_transactions_wallet') THEN
+ ALTER TABLE wallet_transactions
+ ADD CONSTRAINT fk_wallet_transactions_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_orders_shop') THEN
+ ALTER TABLE orders
+ ADD CONSTRAINT fk_orders_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_orders_table') THEN
+ ALTER TABLE orders
+ ADD CONSTRAINT fk_orders_table FOREIGN KEY (table_id) REFERENCES tables(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_items_order') THEN
+ ALTER TABLE order_items
+ ADD CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_items_product') THEN
+ ALTER TABLE order_items
+ ADD CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_payment_transactions_order') THEN
+ ALTER TABLE payment_transactions
+ ADD CONSTRAINT fk_payment_transactions_order FOREIGN KEY (order_id) REFERENCES orders(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_payment_transactions_shop') THEN
+ ALTER TABLE payment_transactions
+ ADD CONSTRAINT fk_payment_transactions_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_returns_order') THEN
+ ALTER TABLE order_returns
+ ADD CONSTRAINT fk_order_returns_order FOREIGN KEY (order_id) REFERENCES orders(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_returns_shop') THEN
+ ALTER TABLE order_returns
+ ADD CONSTRAINT fk_order_returns_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_return_items_return') THEN
+ ALTER TABLE order_return_items
+ ADD CONSTRAINT fk_order_return_items_return FOREIGN KEY (return_id) REFERENCES order_returns(id) ON DELETE CASCADE NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_return_items_order_item') THEN
+ ALTER TABLE order_return_items
+ ADD CONSTRAINT fk_order_return_items_order_item FOREIGN KEY (order_item_id) REFERENCES order_items(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_return_items_product') THEN
+ ALTER TABLE order_return_items
+ ADD CONSTRAINT fk_order_return_items_product FOREIGN KEY (product_id) REFERENCES products(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_refund_transactions_return') THEN
+ ALTER TABLE refund_transactions
+ ADD CONSTRAINT fk_refund_transactions_return FOREIGN KEY (return_id) REFERENCES order_returns(id) ON DELETE SET NULL NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_refund_transactions_order') THEN
+ ALTER TABLE refund_transactions
+ ADD CONSTRAINT fk_refund_transactions_order FOREIGN KEY (order_id) REFERENCES orders(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_refund_transactions_shop') THEN
+ ALTER TABLE refund_transactions
+ ADD CONSTRAINT fk_refund_transactions_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ ALTER TABLE inventory_transactions DROP CONSTRAINT IF EXISTS fk_inventory_transactions_item;
+ ALTER TABLE inventory_transactions
+ ADD CONSTRAINT fk_inventory_transactions_item FOREIGN KEY (inventory_item_id) REFERENCES inventory_items(id) NOT VALID;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_table_sessions_shop') THEN
+ ALTER TABLE table_sessions
+ ADD CONSTRAINT fk_table_sessions_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_table_sessions_table') THEN
+ ALTER TABLE table_sessions
+ ADD CONSTRAINT fk_table_sessions_table FOREIGN KEY (table_id) REFERENCES tables(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_vouchers_campaign') THEN
+ ALTER TABLE vouchers
+ ADD CONSTRAINT fk_vouchers_campaign FOREIGN KEY (campaign_id) REFERENCES campaigns(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_vouchers_shop') THEN
+ ALTER TABLE vouchers
+ ADD CONSTRAINT fk_vouchers_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_vouchers_redeemed_order') THEN
+ ALTER TABLE vouchers
+ ADD CONSTRAINT fk_vouchers_redeemed_order FOREIGN KEY (redeemed_order_id) REFERENCES orders(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_folders_shop') THEN
+ ALTER TABLE storage_folders
+ ADD CONSTRAINT fk_storage_folders_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_folders_parent') THEN
+ ALTER TABLE storage_folders
+ ADD CONSTRAINT fk_storage_folders_parent FOREIGN KEY (parent_id) REFERENCES storage_folders(id) ON DELETE SET NULL NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_files_shop') THEN
+ ALTER TABLE storage_files
+ ADD CONSTRAINT fk_storage_files_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_files_folder') THEN
+ ALTER TABLE storage_files
+ ADD CONSTRAINT fk_storage_files_folder FOREIGN KEY (folder_id) REFERENCES storage_folders(id) ON DELETE SET NULL NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_receipt_templates_shop') THEN
+ ALTER TABLE receipt_templates
+ ADD CONSTRAINT fk_receipt_templates_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_products_price_nonnegative') THEN
+ ALTER TABLE products
+ ADD CONSTRAINT chk_products_price_nonnegative CHECK (price >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_quantity_nonnegative') THEN
+ ALTER TABLE inventory_items
+ ADD CONSTRAINT chk_inventory_items_quantity_nonnegative CHECK (quantity >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_reserved_quantity_nonnegative') THEN
+ ALTER TABLE inventory_items
+ ADD CONSTRAINT chk_inventory_items_reserved_quantity_nonnegative CHECK (reserved_quantity >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_reserved_not_above_quantity') THEN
+ ALTER TABLE inventory_items
+ ADD CONSTRAINT chk_inventory_items_reserved_not_above_quantity CHECK (reserved_quantity <= quantity) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_reorder_level_nonnegative') THEN
+ ALTER TABLE inventory_items
+ ADD CONSTRAINT chk_inventory_items_reorder_level_nonnegative CHECK (reorder_level >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_cost_per_unit_nonnegative') THEN
+ ALTER TABLE inventory_items
+ ADD CONSTRAINT chk_inventory_items_cost_per_unit_nonnegative CHECK (cost_per_unit >= 0) NOT VALID;
+ END IF;
+ ALTER TABLE inventory_transactions DROP CONSTRAINT IF EXISTS chk_inventory_transactions_quantity_nonnegative;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_transactions_quantity_nonzero') THEN
+ ALTER TABLE inventory_transactions
+ ADD CONSTRAINT chk_inventory_transactions_quantity_nonzero CHECK (quantity <> 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_transactions_unit_cost_nonnegative') THEN
+ ALTER TABLE inventory_transactions
+ ADD CONSTRAINT chk_inventory_transactions_unit_cost_nonnegative CHECK (unit_cost IS NULL OR unit_cost >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_tables_capacity_nonnegative') THEN
+ ALTER TABLE tables
+ ADD CONSTRAINT chk_tables_capacity_nonnegative CHECK (capacity >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_tables_hourly_rate_nonnegative') THEN
+ ALTER TABLE tables
+ ADD CONSTRAINT chk_tables_hourly_rate_nonnegative CHECK (hourly_rate >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_orders_amounts_nonnegative') THEN
+ ALTER TABLE orders
+ ADD CONSTRAINT chk_orders_amounts_nonnegative CHECK (
+ total_amount >= 0
+ AND discount_amount >= 0
+ AND (amount_tendered IS NULL OR amount_tendered >= 0)
+ AND (change_amount IS NULL OR change_amount >= 0)
+ ) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_quantity_nonnegative') THEN
+ ALTER TABLE order_items
+ ADD CONSTRAINT chk_order_items_quantity_nonnegative CHECK (quantity >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_unit_price_nonnegative') THEN
+ ALTER TABLE order_items
+ ADD CONSTRAINT chk_order_items_unit_price_nonnegative CHECK (unit_price >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_payment_transactions_amounts_nonnegative') THEN
+ ALTER TABLE payment_transactions
+ ADD CONSTRAINT chk_payment_transactions_amounts_nonnegative CHECK (
+ amount >= 0
+ AND (amount_tendered IS NULL OR amount_tendered >= 0)
+ AND change_amount >= 0
+ ) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_returns_refund_amount_nonnegative') THEN
+ ALTER TABLE order_returns
+ ADD CONSTRAINT chk_order_returns_refund_amount_nonnegative CHECK (refund_amount >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_return_items_quantity_positive') THEN
+ ALTER TABLE order_return_items
+ ADD CONSTRAINT chk_order_return_items_quantity_positive CHECK (quantity > 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_return_items_unit_price_nonnegative') THEN
+ ALTER TABLE order_return_items
+ ADD CONSTRAINT chk_order_return_items_unit_price_nonnegative CHECK (unit_price >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_refund_transactions_amount_positive') THEN
+ ALTER TABLE refund_transactions
+ ADD CONSTRAINT chk_refund_transactions_amount_positive CHECK (amount > 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_wallets_balance_nonnegative') THEN
+ ALTER TABLE wallets
+ ADD CONSTRAINT chk_wallets_balance_nonnegative CHECK (balance >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_campaigns_amounts_nonnegative') THEN
+ ALTER TABLE campaigns
+ ADD CONSTRAINT chk_campaigns_amounts_nonnegative CHECK (
+ face_value >= 0
+ AND discount_value >= 0
+ AND total_vouchers >= 0
+ AND issued_vouchers >= 0
+ ) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_campaigns_date_range') THEN
+ ALTER TABLE campaigns
+ ADD CONSTRAINT chk_campaigns_date_range CHECK (start_date IS NULL OR end_date IS NULL OR end_date >= start_date) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_vouchers_discount_value_nonnegative') THEN
+ ALTER TABLE vouchers
+ ADD CONSTRAINT chk_vouchers_discount_value_nonnegative CHECK (discount_value >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_attendance_records_checkout_after_checkin') THEN
+ ALTER TABLE attendance_records
+ ADD CONSTRAINT chk_attendance_records_checkout_after_checkin CHECK (check_out_at IS NULL OR check_out_at >= check_in_at) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_storage_files_byte_size_nonnegative') THEN
+ ALTER TABLE storage_files
+ ADD CONSTRAINT chk_storage_files_byte_size_nonnegative CHECK (byte_size >= 0) NOT VALID;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_receipt_templates_tax_rate_nonnegative') THEN
+ ALTER TABLE receipt_templates
+ ADD CONSTRAINT chk_receipt_templates_tax_rate_nonnegative CHECK (tax_rate >= 0) NOT VALID;
+ END IF;
+ END
+ $integrity_constraints$;
+
+ INSERT INTO mvp_roles (id, code, name, portal) VALUES
+ (gen_random_uuid(), 'superadmin', 'Super Admin', 'superadmin'),
+ (gen_random_uuid(), 'admin', 'Quản trị cửa hàng', 'admin'),
+ (gen_random_uuid(), 'staff', 'Nhân viên', 'staff'),
+ (gen_random_uuid(), 'marketing', 'Marketing', 'marketing'),
+ (gen_random_uuid(), 'customer', 'Khách hàng', 'customer')
+ ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, portal = EXCLUDED.portal;
+
+ INSERT INTO platform_plans (id, code, name, price, features) VALUES
+ (gen_random_uuid(), 'starter', 'Starter', 0, '["POS", "Catalog", "Inventory"]'::jsonb),
+ (gen_random_uuid(), 'growth', 'Growth', 299000, '["Multi-shop", "Staff", "Reports", "AI"]'::jsonb),
+ (gen_random_uuid(), 'scale', 'Scale', 799000, '["Super admin", "Marketing", "Storage", "Integrations"]'::jsonb)
+ ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, price = EXCLUDED.price, features = EXCLUDED.features;
+
+ INSERT INTO feature_flags (key, description, enabled) VALUES
+ ('ai_chat', 'AI shop assistant with tool calling', true),
+ ('external_storage', 'S3-compatible file storage', true),
+ ('social_marketing', 'Social publishing adapters', true),
+ ('customer_qr_ordering', 'Customer QR menu ordering', true)
+ ON CONFLICT (key) DO UPDATE SET description = EXCLUDED.description, enabled = EXCLUDED.enabled, updated_at = now();
+ `;
diff --git a/microservices/apps/tpos-mvp-next/src/server/db/schema-statements.ts b/microservices/apps/tpos-mvp-next/src/server/db/schema-statements.ts
new file mode 100644
index 00000000..bba1bdb3
--- /dev/null
+++ b/microservices/apps/tpos-mvp-next/src/server/db/schema-statements.ts
@@ -0,0 +1,9 @@
+import { coreSchemaSqlPart1 } from "./schema-sql-part-1";
+import { coreSchemaSqlPart2 } from "./schema-sql-part-2";
+import { coreSchemaSqlPart3 } from "./schema-sql-part-3";
+
+export const coreSchemaSql = [
+ coreSchemaSqlPart1,
+ coreSchemaSqlPart2,
+ coreSchemaSqlPart3
+].join("\n");
diff --git a/microservices/apps/tpos-mvp-next/src/server/db/schema.ts b/microservices/apps/tpos-mvp-next/src/server/db/schema.ts
index 19ad160c..d31bcd28 100644
--- a/microservices/apps/tpos-mvp-next/src/server/db/schema.ts
+++ b/microservices/apps/tpos-mvp-next/src/server/db/schema.ts
@@ -1,1159 +1,6 @@
import type { Pool } from "pg";
+import { coreSchemaSql } from "./schema-statements";
export async function createCoreSchema(pool: Pool) {
- await pool.query(`
- CREATE EXTENSION IF NOT EXISTS pgcrypto;
-
- CREATE TABLE IF NOT EXISTS merchant_types (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS merchant_statuses (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS verification_statuses (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS shop_types (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS shop_statuses (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS business_categories (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS product_types (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS order_statuses (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS item_types (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS transaction_types (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- CREATE TABLE IF NOT EXISTS table_statuses (
- id integer PRIMARY KEY,
- name varchar(50) NOT NULL
- );
-
- INSERT INTO merchant_types (id, name) VALUES (1, 'Individual'), (2, 'Company')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO merchant_statuses (id, name) VALUES (1, 'PendingApproval'), (2, 'Active'), (3, 'Suspended'), (4, 'Banned')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO verification_statuses (id, name) VALUES (1, 'Unverified'), (2, 'Pending'), (3, 'Verified'), (4, 'Rejected')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO shop_types (id, name) VALUES (1, 'OnlineOnly'), (2, 'PhysicalOnly'), (3, 'Hybrid')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO shop_statuses (id, name) VALUES (1, 'Draft'), (2, 'Active'), (3, 'Inactive'), (4, 'Closed')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO business_categories (id, name) VALUES
- (1, 'FoodBeverage'), (2, 'Fashion'), (3, 'Electronics'), (4, 'Healthcare'),
- (5, 'Beauty'), (6, 'Education'), (7, 'Entertainment'), (8, 'Services'),
- (9, 'Grocery'), (10, 'HomeFurniture'), (11, 'Cafe'), (12, 'Restaurant'),
- (13, 'Karaoke'), (14, 'Spa'), (99, 'Other')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO product_types (id, name) VALUES (1, 'Physical'), (2, 'Service'), (3, 'PreparedFood')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO order_statuses (id, name) VALUES
- (1, 'Draft'), (2, 'Validated'), (3, 'Paid'), (4, 'Processing'),
- (5, 'Completed'), (6, 'Cancelled'), (7, 'PaymentPending'), (8, 'Returned')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO item_types (id, name) VALUES (1, 'RawMaterial'), (2, 'FinishedGood'), (3, 'Consumable')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO transaction_types (id, name) VALUES (1, 'In'), (2, 'Out'), (3, 'Adjustment'), (4, 'Reserve'), (5, 'Release')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
- INSERT INTO table_statuses (id, name) VALUES (1, 'Available'), (2, 'Occupied'), (3, 'Reserved'), (4, 'Cleaning')
- ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
-
- CREATE TABLE IF NOT EXISTS merchants (
- id uuid PRIMARY KEY,
- user_id uuid NOT NULL,
- business_name varchar(200) NOT NULL,
- type_id integer NOT NULL DEFAULT 1,
- status_id integer NOT NULL DEFAULT 2,
- verification_status_id integer NOT NULL DEFAULT 1,
- subscription_plan_id integer NOT NULL DEFAULT 0,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz,
- is_deleted boolean NOT NULL DEFAULT false
- );
- ALTER TABLE merchants ADD COLUMN IF NOT EXISTS subscription_plan_id integer NOT NULL DEFAULT 0;
- ALTER TABLE merchants ADD COLUMN IF NOT EXISTS is_deleted boolean NOT NULL DEFAULT false;
-
- CREATE TABLE IF NOT EXISTS shops (
- id uuid PRIMARY KEY,
- merchant_id uuid NOT NULL,
- name varchar(100) NOT NULL,
- slug varchar(100) NOT NULL,
- type_id integer NOT NULL DEFAULT 2,
- category_id integer NOT NULL DEFAULT 9,
- status_id integer NOT NULL DEFAULT 2,
- description varchar(2000),
- phone varchar(20),
- email varchar(100),
- website varchar(200),
- logo_url varchar(500),
- cover_image_url varchar(500),
- features_config jsonb,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz,
- is_default boolean NOT NULL DEFAULT false,
- is_deleted boolean NOT NULL DEFAULT false
- );
- ALTER TABLE shops ADD COLUMN IF NOT EXISTS phone varchar(20);
- ALTER TABLE shops ADD COLUMN IF NOT EXISTS email varchar(100);
- ALTER TABLE shops ADD COLUMN IF NOT EXISTS website varchar(200);
- ALTER TABLE shops ADD COLUMN IF NOT EXISTS features_config jsonb;
- ALTER TABLE shops ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false;
- ALTER TABLE shops ADD COLUMN IF NOT EXISTS is_deleted boolean NOT NULL DEFAULT false;
-
- CREATE TABLE IF NOT EXISTS categories (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- name varchar(200) NOT NULL,
- description varchar(1000),
- parent_id uuid,
- display_order integer NOT NULL DEFAULT 0,
- image_url varchar(500),
- is_active boolean NOT NULL DEFAULT true,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
- ALTER TABLE categories ADD COLUMN IF NOT EXISTS image_url varchar(500);
- ALTER TABLE categories ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
-
- CREATE TABLE IF NOT EXISTS products (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- name varchar(255) NOT NULL,
- description varchar(2000),
- price numeric(18,2) NOT NULL,
- type_id integer NOT NULL DEFAULT 1,
- attributes jsonb,
- image_url varchar(500),
- sku varchar(100),
- barcode varchar(100),
- category_id uuid,
- is_active boolean NOT NULL DEFAULT true,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
- ALTER TABLE products ADD COLUMN IF NOT EXISTS barcode varchar(100);
- ALTER TABLE products ADD COLUMN IF NOT EXISTS category_id uuid;
- ALTER TABLE products ADD COLUMN IF NOT EXISTS image_url varchar(500);
- ALTER TABLE products ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
-
- CREATE TABLE IF NOT EXISTS inventory_items (
- id uuid PRIMARY KEY,
- product_id uuid NOT NULL,
- shop_id uuid NOT NULL,
- name varchar(200),
- item_type_id integer NOT NULL DEFAULT 2,
- unit varchar(20) NOT NULL DEFAULT 'pcs',
- cost_per_unit numeric(18,4) NOT NULL DEFAULT 0,
- supplier_name varchar(200),
- expiry_date timestamptz,
- quantity integer NOT NULL DEFAULT 0,
- reserved_quantity integer NOT NULL DEFAULT 0,
- reorder_level integer NOT NULL DEFAULT 10,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz,
- deleted_at timestamptz
- );
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS name varchar(200);
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS item_type_id integer NOT NULL DEFAULT 2;
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS unit varchar(20) NOT NULL DEFAULT 'pcs';
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS cost_per_unit numeric(18,4) NOT NULL DEFAULT 0;
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS supplier_name varchar(200);
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS expiry_date timestamptz;
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
- ALTER TABLE inventory_items ADD COLUMN IF NOT EXISTS deleted_at timestamptz;
-
- CREATE TABLE IF NOT EXISTS inventory_transactions (
- id uuid PRIMARY KEY,
- inventory_item_id uuid NOT NULL,
- type_id integer NOT NULL,
- quantity integer NOT NULL,
- reference_id uuid,
- notes varchar(500),
- invoice_image_url varchar(1000),
- unit_cost numeric(18,4),
- created_at timestamptz NOT NULL DEFAULT now()
- );
- ALTER TABLE inventory_transactions ADD COLUMN IF NOT EXISTS invoice_image_url varchar(1000);
- ALTER TABLE inventory_transactions ADD COLUMN IF NOT EXISTS unit_cost numeric(18,4);
-
- CREATE TABLE IF NOT EXISTS tables (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- table_number varchar(20) NOT NULL,
- capacity integer NOT NULL DEFAULT 2,
- zone varchar(100),
- status_id integer NOT NULL DEFAULT 1,
- position_x integer,
- position_y integer,
- qr_token varchar(64),
- hourly_rate numeric(18,2) NOT NULL DEFAULT 0,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS orders (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- customer_id uuid,
- table_id uuid,
- status_id integer NOT NULL DEFAULT 1,
- total_amount numeric(18,2) NOT NULL DEFAULT 0,
- notes varchar(2000),
- discount_amount numeric(18,2) NOT NULL DEFAULT 0,
- discount_type varchar(50),
- discount_reference varchar(255),
- payment_method varchar(50),
- transaction_id varchar(255),
- amount_tendered numeric(18,2),
- change_amount numeric(18,2),
- return_reason varchar(1000),
- returned_at timestamptz,
- is_return boolean NOT NULL DEFAULT false,
- original_order_id uuid,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS table_id uuid;
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_method varchar(50);
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS transaction_id varchar(255);
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS amount_tendered numeric(18,2);
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS change_amount numeric(18,2);
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS discount_amount numeric(18,2) NOT NULL DEFAULT 0;
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS discount_type varchar(50);
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS discount_reference varchar(255);
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS return_reason varchar(1000);
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS returned_at timestamptz;
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS is_return boolean NOT NULL DEFAULT false;
- ALTER TABLE orders ADD COLUMN IF NOT EXISTS original_order_id uuid;
-
- CREATE TABLE IF NOT EXISTS order_items (
- id uuid PRIMARY KEY,
- order_id uuid NOT NULL,
- product_id uuid NOT NULL,
- product_name varchar(255) NOT NULL,
- product_type varchar(50) NOT NULL,
- quantity integer NOT NULL,
- unit_price numeric(18,2) NOT NULL,
- status varchar(50) NOT NULL DEFAULT 'Completed',
- track_inventory boolean NOT NULL DEFAULT true,
- metadata jsonb
- );
- ALTER TABLE order_items ADD COLUMN IF NOT EXISTS track_inventory boolean NOT NULL DEFAULT true;
-
- CREATE TABLE IF NOT EXISTS payment_transactions (
- id uuid PRIMARY KEY,
- order_id uuid NOT NULL,
- shop_id uuid NOT NULL,
- method varchar(50) NOT NULL,
- amount numeric(18,2) NOT NULL,
- amount_tendered numeric(18,2),
- change_amount numeric(18,2) NOT NULL DEFAULT 0,
- status varchar(50) NOT NULL DEFAULT 'Succeeded',
- provider_reference varchar(255),
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS order_returns (
- id uuid PRIMARY KEY,
- order_id uuid NOT NULL,
- shop_id uuid NOT NULL,
- return_type varchar(50) NOT NULL DEFAULT 'return',
- reason varchar(1000),
- refund_amount numeric(18,2) NOT NULL DEFAULT 0,
- status varchar(50) NOT NULL DEFAULT 'completed',
- idempotency_key varchar(128),
- request_hash char(64),
- created_by uuid,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS return_type varchar(50) NOT NULL DEFAULT 'return';
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS reason varchar(1000);
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS refund_amount numeric(18,2) NOT NULL DEFAULT 0;
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS status varchar(50) NOT NULL DEFAULT 'completed';
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS idempotency_key varchar(128);
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS request_hash char(64);
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS created_by uuid;
- ALTER TABLE order_returns ADD COLUMN IF NOT EXISTS updated_at timestamptz;
-
- CREATE TABLE IF NOT EXISTS order_return_items (
- id uuid PRIMARY KEY,
- return_id uuid NOT NULL,
- order_item_id uuid NOT NULL,
- product_id uuid NOT NULL,
- product_name varchar(255) NOT NULL,
- quantity integer NOT NULL,
- unit_price numeric(18,2) NOT NULL,
- restock boolean NOT NULL DEFAULT false,
- created_at timestamptz NOT NULL DEFAULT now()
- );
- ALTER TABLE order_return_items ADD COLUMN IF NOT EXISTS restock boolean NOT NULL DEFAULT false;
-
- CREATE TABLE IF NOT EXISTS refund_transactions (
- id uuid PRIMARY KEY,
- return_id uuid,
- order_id uuid NOT NULL,
- shop_id uuid NOT NULL,
- method varchar(50) NOT NULL DEFAULT 'cash',
- amount numeric(18,2) NOT NULL,
- status varchar(50) NOT NULL DEFAULT 'Succeeded',
- reason varchar(1000),
- provider_reference varchar(255),
- created_at timestamptz NOT NULL DEFAULT now()
- );
- ALTER TABLE refund_transactions ADD COLUMN IF NOT EXISTS return_id uuid;
- ALTER TABLE refund_transactions ADD COLUMN IF NOT EXISTS reason varchar(1000);
- ALTER TABLE refund_transactions ADD COLUMN IF NOT EXISTS provider_reference varchar(255);
-
- CREATE TABLE IF NOT EXISTS mvp_roles (
- id uuid PRIMARY KEY,
- code varchar(50) NOT NULL UNIQUE,
- name varchar(100) NOT NULL,
- portal varchar(50) NOT NULL,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS mvp_users (
- id uuid PRIMARY KEY,
- email varchar(200) NOT NULL UNIQUE,
- password_hash varchar(255) NOT NULL,
- display_name varchar(200) NOT NULL,
- phone varchar(50),
- status varchar(50) NOT NULL DEFAULT 'active',
- default_shop_id uuid,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS mvp_user_roles (
- user_id uuid NOT NULL,
- role_id uuid NOT NULL,
- shop_id uuid
- );
-
- CREATE TABLE IF NOT EXISTS mvp_sessions (
- id uuid PRIMARY KEY,
- user_id uuid NOT NULL,
- token_hash varchar(255) NOT NULL UNIQUE,
- expires_at timestamptz NOT NULL,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS staff_members (
- id uuid PRIMARY KEY,
- user_id uuid,
- shop_id uuid,
- employee_code varchar(50),
- first_name varchar(100),
- last_name varchar(100),
- phone varchar(50),
- email varchar(200),
- role varchar(80),
- status varchar(50) NOT NULL DEFAULT 'active',
- joined_at timestamptz NOT NULL DEFAULT now(),
- terminated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS staff_schedules (
- id uuid PRIMARY KEY,
- staff_id uuid NOT NULL,
- shop_id uuid NOT NULL,
- day_of_week integer NOT NULL,
- start_time varchar(20) NOT NULL,
- end_time varchar(20) NOT NULL,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS attendance_records (
- id uuid PRIMARY KEY,
- staff_id uuid NOT NULL,
- shop_id uuid,
- check_in_at timestamptz NOT NULL DEFAULT now(),
- check_out_at timestamptz,
- status varchar(50) NOT NULL DEFAULT 'checked_in',
- note varchar(500)
- );
-
- CREATE TABLE IF NOT EXISTS leave_requests (
- id uuid PRIMARY KEY,
- staff_id uuid NOT NULL,
- shop_id uuid,
- from_date date NOT NULL,
- to_date date NOT NULL,
- reason varchar(500),
- status varchar(50) NOT NULL DEFAULT 'pending',
- reviewed_by uuid,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS notifications (
- id uuid PRIMARY KEY,
- user_id uuid,
- shop_id uuid,
- title varchar(200) NOT NULL,
- body varchar(1000),
- status varchar(50) NOT NULL DEFAULT 'unread',
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS members (
- id uuid PRIMARY KEY,
- shop_id uuid,
- display_name varchar(200),
- phone varchar(50),
- gender varchar(50),
- country_code varchar(10),
- current_exp integer NOT NULL DEFAULT 0,
- current_level integer NOT NULL DEFAULT 1,
- total_exp_earned integer NOT NULL DEFAULT 0,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS membership_levels (
- id uuid PRIMARY KEY,
- shop_id uuid,
- level_number integer NOT NULL,
- name varchar(100) NOT NULL,
- required_exp integer NOT NULL DEFAULT 0,
- description varchar(500),
- badge_color varchar(30),
- is_active boolean NOT NULL DEFAULT true
- );
-
- CREATE TABLE IF NOT EXISTS experience_transactions (
- id uuid PRIMARY KEY,
- member_id uuid NOT NULL,
- points integer NOT NULL,
- source varchar(80) NOT NULL,
- reference_id varchar(100),
- level_at_time integer NOT NULL DEFAULT 1,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS wallets (
- id uuid PRIMARY KEY,
- owner_id uuid NOT NULL,
- currency varchar(10) NOT NULL DEFAULT 'VND',
- balance numeric(18,2) NOT NULL DEFAULT 0,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS wallet_transactions (
- id uuid PRIMARY KEY,
- wallet_id uuid NOT NULL,
- amount numeric(18,2) NOT NULL,
- description varchar(500),
- item_name varchar(200),
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS campaigns (
- id uuid PRIMARY KEY,
- shop_id uuid,
- name varchar(200) NOT NULL,
- description varchar(1000),
- face_value numeric(18,2) NOT NULL DEFAULT 0,
- discount_type varchar(50) NOT NULL DEFAULT 'fixed',
- discount_value numeric(18,2) NOT NULL DEFAULT 0,
- total_vouchers integer NOT NULL DEFAULT 0,
- issued_vouchers integer NOT NULL DEFAULT 0,
- status varchar(50) NOT NULL DEFAULT 'draft',
- start_date timestamptz,
- end_date timestamptz,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS vouchers (
- id uuid PRIMARY KEY,
- campaign_id uuid,
- shop_id uuid,
- code varchar(100) NOT NULL UNIQUE,
- status varchar(50) NOT NULL DEFAULT 'active',
- discount_type varchar(50) NOT NULL DEFAULT 'fixed',
- discount_value numeric(18,2) NOT NULL DEFAULT 0,
- redeemed_order_id uuid,
- redeemed_at timestamptz,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS resources (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- name varchar(200) NOT NULL,
- resource_type varchar(80),
- capacity integer NOT NULL DEFAULT 1,
- is_active boolean NOT NULL DEFAULT true,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS therapists (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- staff_id uuid,
- name varchar(200) NOT NULL,
- specialty varchar(200),
- status varchar(50) NOT NULL DEFAULT 'active',
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS appointments (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- customer_id uuid,
- staff_id uuid,
- resource_id uuid,
- service_id uuid,
- customer_name varchar(200),
- service_name varchar(200),
- therapist_name varchar(200),
- resource_name varchar(200),
- start_time timestamptz NOT NULL,
- end_time timestamptz NOT NULL,
- status varchar(50) NOT NULL DEFAULT 'pending',
- notes varchar(1000),
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS recipes (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- product_id uuid,
- name varchar(200) NOT NULL,
- ingredients jsonb NOT NULL DEFAULT '[]'::jsonb,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS kitchen_tickets (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- order_id uuid,
- table_id uuid,
- table_label varchar(100),
- status varchar(50) NOT NULL DEFAULT 'Pending',
- priority varchar(50) NOT NULL DEFAULT 'normal',
- items jsonb NOT NULL DEFAULT '[]'::jsonb,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS barista_queue (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- order_id uuid,
- product_name varchar(200) NOT NULL,
- customer_name varchar(200),
- status varchar(50) NOT NULL DEFAULT 'Pending',
- barista_name varchar(200),
- created_at timestamptz NOT NULL DEFAULT now(),
- started_at timestamptz,
- ready_at timestamptz,
- delivered_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS reservations (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- table_id uuid,
- customer_name varchar(200) NOT NULL,
- phone varchar(50),
- guest_count integer NOT NULL DEFAULT 1,
- reservation_time timestamptz NOT NULL,
- status varchar(50) NOT NULL DEFAULT 'pending',
- notes varchar(1000),
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS table_sessions (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- table_id uuid NOT NULL,
- status varchar(50) NOT NULL DEFAULT 'open',
- guest_count integer NOT NULL DEFAULT 1,
- started_at timestamptz NOT NULL DEFAULT now(),
- closed_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS storage_folders (
- id uuid PRIMARY KEY,
- shop_id uuid,
- parent_id uuid,
- name varchar(200) NOT NULL,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS storage_files (
- id uuid PRIMARY KEY,
- shop_id uuid,
- folder_id uuid,
- file_name varchar(255) NOT NULL,
- content_type varchar(120),
- byte_size bigint NOT NULL DEFAULT 0,
- object_key varchar(500) NOT NULL,
- access_level varchar(50) NOT NULL DEFAULT 'public',
- public_url varchar(1000),
- provider varchar(50) NOT NULL DEFAULT 's3',
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS ai_configs (
- shop_id uuid PRIMARY KEY,
- provider varchar(50) NOT NULL,
- api_key_ref varchar(255),
- model varchar(100) NOT NULL,
- base_url varchar(500),
- system_prompt text,
- enabled boolean NOT NULL DEFAULT true,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS ai_messages (
- id uuid PRIMARY KEY,
- shop_id uuid,
- user_id uuid,
- role varchar(50) NOT NULL,
- content text NOT NULL,
- tools_used jsonb,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS platform_plans (
- id uuid PRIMARY KEY,
- code varchar(50) NOT NULL UNIQUE,
- name varchar(100) NOT NULL,
- price numeric(18,2) NOT NULL DEFAULT 0,
- features jsonb NOT NULL DEFAULT '[]'::jsonb,
- is_active boolean NOT NULL DEFAULT true
- );
-
- CREATE TABLE IF NOT EXISTS feature_flags (
- key varchar(100) PRIMARY KEY,
- description varchar(500),
- enabled boolean NOT NULL DEFAULT false,
- updated_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS audit_logs (
- id uuid PRIMARY KEY,
- actor_user_id uuid,
- action varchar(150) NOT NULL,
- entity_type varchar(100),
- entity_id varchar(100),
- metadata jsonb,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS social_connections (
- id uuid PRIMARY KEY,
- shop_id uuid,
- provider varchar(50) NOT NULL,
- account_name varchar(200),
- external_id varchar(200),
- status varchar(50) NOT NULL DEFAULT 'configured',
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
-
- CREATE TABLE IF NOT EXISTS social_posts (
- id uuid PRIMARY KEY,
- shop_id uuid,
- provider varchar(50) NOT NULL,
- content text NOT NULL,
- status varchar(50) NOT NULL DEFAULT 'draft',
- external_id varchar(200),
- scheduled_at timestamptz,
- published_at timestamptz,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE TABLE IF NOT EXISTS receipt_templates (
- id uuid PRIMARY KEY,
- shop_id uuid NOT NULL,
- code varchar(80) NOT NULL,
- name varchar(200) NOT NULL,
- template_type varchar(50) NOT NULL DEFAULT 'sales_receipt',
- paper_size varchar(50) NOT NULL DEFAULT '80mm',
- header_text varchar(500),
- footer_text varchar(1000),
- tax_label varchar(120),
- tax_rate numeric(8,4) NOT NULL DEFAULT 0,
- show_logo boolean NOT NULL DEFAULT true,
- show_qr boolean NOT NULL DEFAULT true,
- show_tax boolean NOT NULL DEFAULT false,
- kitchen_copy boolean NOT NULL DEFAULT false,
- is_default boolean NOT NULL DEFAULT false,
- is_active boolean NOT NULL DEFAULT true,
- metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz
- );
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS template_type varchar(50) NOT NULL DEFAULT 'sales_receipt';
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS paper_size varchar(50) NOT NULL DEFAULT '80mm';
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS header_text varchar(500);
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS footer_text varchar(1000);
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS tax_label varchar(120);
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS tax_rate numeric(8,4) NOT NULL DEFAULT 0;
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS show_logo boolean NOT NULL DEFAULT true;
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS show_qr boolean NOT NULL DEFAULT true;
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS show_tax boolean NOT NULL DEFAULT false;
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS kitchen_copy boolean NOT NULL DEFAULT false;
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false;
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
- ALTER TABLE receipt_templates ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb;
-
- CREATE TABLE IF NOT EXISTS mvp_activity (
- id uuid PRIMARY KEY,
- action varchar(100) NOT NULL,
- entity_type varchar(100) NOT NULL,
- entity_id uuid,
- shop_id uuid,
- payload jsonb,
- created_at timestamptz NOT NULL DEFAULT now()
- );
-
- CREATE UNIQUE INDEX IF NOT EXISTS ix_shops_slug ON shops(slug);
- CREATE INDEX IF NOT EXISTS ix_shops_merchant_id ON shops(merchant_id);
- CREATE INDEX IF NOT EXISTS ix_categories_shop_id ON categories(shop_id);
- CREATE INDEX IF NOT EXISTS ix_products_shop_id ON products(shop_id);
- CREATE INDEX IF NOT EXISTS ix_products_barcode ON products(barcode);
- CREATE INDEX IF NOT EXISTS ix_inventory_shop_id ON inventory_items(shop_id);
- CREATE INDEX IF NOT EXISTS ix_inventory_product_id ON inventory_items(product_id);
- CREATE INDEX IF NOT EXISTS ix_orders_shop_id ON orders(shop_id);
- CREATE INDEX IF NOT EXISTS ix_orders_created_at ON orders(created_at);
- CREATE INDEX IF NOT EXISTS ix_order_items_order_id ON order_items(order_id);
- CREATE INDEX IF NOT EXISTS ix_payment_transactions_order_id ON payment_transactions(order_id);
- CREATE INDEX IF NOT EXISTS ix_payment_transactions_shop_id ON payment_transactions(shop_id);
- CREATE INDEX IF NOT EXISTS ix_order_returns_order_id ON order_returns(order_id);
- CREATE INDEX IF NOT EXISTS ix_order_returns_shop_id ON order_returns(shop_id);
- CREATE UNIQUE INDEX IF NOT EXISTS ux_order_returns_shop_idempotency_key
- ON order_returns(shop_id, idempotency_key)
- WHERE idempotency_key IS NOT NULL;
- CREATE INDEX IF NOT EXISTS ix_order_return_items_return_id ON order_return_items(return_id);
- CREATE INDEX IF NOT EXISTS ix_order_return_items_order_item_id ON order_return_items(order_item_id);
- CREATE INDEX IF NOT EXISTS ix_refund_transactions_order_id ON refund_transactions(order_id);
- CREATE INDEX IF NOT EXISTS ix_refund_transactions_shop_id ON refund_transactions(shop_id);
- CREATE UNIQUE INDEX IF NOT EXISTS ix_tables_shop_table_number ON tables(shop_id, table_number);
- CREATE INDEX IF NOT EXISTS ix_mvp_sessions_token_hash ON mvp_sessions(token_hash);
- CREATE UNIQUE INDEX IF NOT EXISTS ux_mvp_user_roles_scope ON mvp_user_roles(user_id, role_id, COALESCE(shop_id, '00000000-0000-0000-0000-000000000000'::uuid));
-
- WITH duplicate_staff_codes AS (
- SELECT
- id,
- employee_code,
- row_number() OVER (PARTITION BY shop_id, employee_code ORDER BY joined_at NULLS LAST, id) AS duplicate_rank
- FROM staff_members
- WHERE employee_code IS NOT NULL AND btrim(employee_code) <> ''
- )
- UPDATE staff_members staff
- SET employee_code = left(duplicate_staff_codes.employee_code, 43) || '-' || substr(staff.id::text, 1, 6)
- FROM duplicate_staff_codes
- WHERE staff.id = duplicate_staff_codes.id
- AND duplicate_staff_codes.duplicate_rank > 1;
-
- CREATE UNIQUE INDEX IF NOT EXISTS ux_staff_members_shop_employee_code ON staff_members(shop_id, employee_code);
- CREATE INDEX IF NOT EXISTS ix_staff_members_shop_id ON staff_members(shop_id);
- CREATE INDEX IF NOT EXISTS ix_attendance_staff_id ON attendance_records(staff_id);
- CREATE INDEX IF NOT EXISTS ix_members_shop_id ON members(shop_id);
- CREATE INDEX IF NOT EXISTS ix_campaigns_shop_id ON campaigns(shop_id);
- CREATE INDEX IF NOT EXISTS ix_vouchers_code ON vouchers(code);
- CREATE INDEX IF NOT EXISTS ix_appointments_shop_id ON appointments(shop_id);
- CREATE INDEX IF NOT EXISTS ix_kitchen_tickets_shop_id ON kitchen_tickets(shop_id);
- CREATE INDEX IF NOT EXISTS ix_barista_queue_shop_id ON barista_queue(shop_id);
- CREATE INDEX IF NOT EXISTS ix_reservations_shop_id ON reservations(shop_id);
- CREATE INDEX IF NOT EXISTS ix_storage_files_shop_id ON storage_files(shop_id);
- CREATE INDEX IF NOT EXISTS ix_ai_messages_shop_id ON ai_messages(shop_id);
- CREATE INDEX IF NOT EXISTS ix_receipt_templates_shop_id ON receipt_templates(shop_id);
- CREATE INDEX IF NOT EXISTS ix_audit_logs_created_at ON audit_logs(created_at DESC);
- CREATE INDEX IF NOT EXISTS ix_mvp_activity_created_at ON mvp_activity(created_at DESC);
- CREATE UNIQUE INDEX IF NOT EXISTS ux_receipt_templates_shop_code ON receipt_templates(shop_id, code);
-
- DO $integrity_indexes$
- BEGIN
- IF to_regclass('public.ux_tables_qr_token_not_null') IS NULL THEN
- IF NOT EXISTS (
- SELECT 1
- FROM tables
- WHERE qr_token IS NOT NULL
- GROUP BY qr_token
- HAVING COUNT(*) > 1
- ) THEN
- CREATE UNIQUE INDEX ux_tables_qr_token_not_null ON tables(qr_token) WHERE qr_token IS NOT NULL;
- ELSE
- RAISE NOTICE 'Skipping ux_tables_qr_token_not_null because duplicate qr_token values exist.';
- END IF;
- END IF;
-
- IF to_regclass('public.ux_wallets_owner_currency') IS NULL THEN
- IF NOT EXISTS (
- SELECT 1
- FROM wallets
- GROUP BY owner_id, currency
- HAVING COUNT(*) > 1
- ) THEN
- CREATE UNIQUE INDEX ux_wallets_owner_currency ON wallets(owner_id, currency);
- ELSE
- RAISE NOTICE 'Skipping ux_wallets_owner_currency because duplicate owner/currency wallets exist.';
- END IF;
- END IF;
-
- IF to_regclass('public.ux_payment_transactions_success_order') IS NULL THEN
- IF NOT EXISTS (
- SELECT 1
- FROM payment_transactions
- WHERE status = 'Succeeded'
- GROUP BY order_id
- HAVING COUNT(*) > 1
- ) THEN
- CREATE UNIQUE INDEX ux_payment_transactions_success_order
- ON payment_transactions(order_id)
- WHERE status = 'Succeeded';
- ELSE
- RAISE NOTICE 'Skipping ux_payment_transactions_success_order because duplicate successful payments exist.';
- END IF;
- END IF;
-
- IF to_regclass('public.ux_payment_transactions_terminal_order') IS NULL THEN
- IF NOT EXISTS (
- SELECT 1
- FROM payment_transactions
- WHERE lower(status) IN ('succeeded', 'completed')
- GROUP BY order_id
- HAVING COUNT(*) > 1
- ) THEN
- CREATE UNIQUE INDEX ux_payment_transactions_terminal_order
- ON payment_transactions(order_id)
- WHERE lower(status) IN ('succeeded', 'completed');
- ELSE
- RAISE NOTICE 'Skipping ux_payment_transactions_terminal_order because duplicate terminal payments exist.';
- END IF;
- END IF;
-
- IF to_regclass('public.ux_table_sessions_one_open') IS NULL THEN
- IF NOT EXISTS (
- SELECT 1
- FROM table_sessions
- WHERE lower(status) = 'open'
- GROUP BY table_id
- HAVING COUNT(*) > 1
- ) THEN
- CREATE UNIQUE INDEX ux_table_sessions_one_open
- ON table_sessions(table_id)
- WHERE lower(status) = 'open';
- ELSE
- RAISE NOTICE 'Skipping ux_table_sessions_one_open because duplicate open table sessions exist.';
- END IF;
- END IF;
-
- DROP INDEX IF EXISTS ux_inventory_items_shop_product;
- IF to_regclass('public.ux_inventory_items_shop_product') IS NULL THEN
- IF NOT EXISTS (
- SELECT 1
- FROM inventory_items
- WHERE product_id IS NOT NULL AND deleted_at IS NULL
- GROUP BY shop_id, product_id
- HAVING COUNT(*) > 1
- ) THEN
- CREATE UNIQUE INDEX ux_inventory_items_shop_product
- ON inventory_items(shop_id, product_id)
- WHERE product_id IS NOT NULL AND deleted_at IS NULL;
- ELSE
- RAISE NOTICE 'Skipping ux_inventory_items_shop_product because duplicate shop/product inventory rows exist.';
- END IF;
- END IF;
- END
- $integrity_indexes$;
-
- DO $integrity_constraints$
- BEGIN
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_staff_members_user') THEN
- ALTER TABLE staff_members
- ADD CONSTRAINT fk_staff_members_user FOREIGN KEY (user_id) REFERENCES mvp_users(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_staff_members_shop') THEN
- ALTER TABLE staff_members
- ADD CONSTRAINT fk_staff_members_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_wallets_owner') THEN
- ALTER TABLE wallets
- ADD CONSTRAINT fk_wallets_owner FOREIGN KEY (owner_id) REFERENCES mvp_users(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_wallet_transactions_wallet') THEN
- ALTER TABLE wallet_transactions
- ADD CONSTRAINT fk_wallet_transactions_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_orders_shop') THEN
- ALTER TABLE orders
- ADD CONSTRAINT fk_orders_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_orders_table') THEN
- ALTER TABLE orders
- ADD CONSTRAINT fk_orders_table FOREIGN KEY (table_id) REFERENCES tables(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_items_order') THEN
- ALTER TABLE order_items
- ADD CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_items_product') THEN
- ALTER TABLE order_items
- ADD CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_payment_transactions_order') THEN
- ALTER TABLE payment_transactions
- ADD CONSTRAINT fk_payment_transactions_order FOREIGN KEY (order_id) REFERENCES orders(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_payment_transactions_shop') THEN
- ALTER TABLE payment_transactions
- ADD CONSTRAINT fk_payment_transactions_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_returns_order') THEN
- ALTER TABLE order_returns
- ADD CONSTRAINT fk_order_returns_order FOREIGN KEY (order_id) REFERENCES orders(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_returns_shop') THEN
- ALTER TABLE order_returns
- ADD CONSTRAINT fk_order_returns_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_return_items_return') THEN
- ALTER TABLE order_return_items
- ADD CONSTRAINT fk_order_return_items_return FOREIGN KEY (return_id) REFERENCES order_returns(id) ON DELETE CASCADE NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_return_items_order_item') THEN
- ALTER TABLE order_return_items
- ADD CONSTRAINT fk_order_return_items_order_item FOREIGN KEY (order_item_id) REFERENCES order_items(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_order_return_items_product') THEN
- ALTER TABLE order_return_items
- ADD CONSTRAINT fk_order_return_items_product FOREIGN KEY (product_id) REFERENCES products(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_refund_transactions_return') THEN
- ALTER TABLE refund_transactions
- ADD CONSTRAINT fk_refund_transactions_return FOREIGN KEY (return_id) REFERENCES order_returns(id) ON DELETE SET NULL NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_refund_transactions_order') THEN
- ALTER TABLE refund_transactions
- ADD CONSTRAINT fk_refund_transactions_order FOREIGN KEY (order_id) REFERENCES orders(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_refund_transactions_shop') THEN
- ALTER TABLE refund_transactions
- ADD CONSTRAINT fk_refund_transactions_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- ALTER TABLE inventory_transactions DROP CONSTRAINT IF EXISTS fk_inventory_transactions_item;
- ALTER TABLE inventory_transactions
- ADD CONSTRAINT fk_inventory_transactions_item FOREIGN KEY (inventory_item_id) REFERENCES inventory_items(id) NOT VALID;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_table_sessions_shop') THEN
- ALTER TABLE table_sessions
- ADD CONSTRAINT fk_table_sessions_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_table_sessions_table') THEN
- ALTER TABLE table_sessions
- ADD CONSTRAINT fk_table_sessions_table FOREIGN KEY (table_id) REFERENCES tables(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_vouchers_campaign') THEN
- ALTER TABLE vouchers
- ADD CONSTRAINT fk_vouchers_campaign FOREIGN KEY (campaign_id) REFERENCES campaigns(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_vouchers_shop') THEN
- ALTER TABLE vouchers
- ADD CONSTRAINT fk_vouchers_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_vouchers_redeemed_order') THEN
- ALTER TABLE vouchers
- ADD CONSTRAINT fk_vouchers_redeemed_order FOREIGN KEY (redeemed_order_id) REFERENCES orders(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_folders_shop') THEN
- ALTER TABLE storage_folders
- ADD CONSTRAINT fk_storage_folders_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_folders_parent') THEN
- ALTER TABLE storage_folders
- ADD CONSTRAINT fk_storage_folders_parent FOREIGN KEY (parent_id) REFERENCES storage_folders(id) ON DELETE SET NULL NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_files_shop') THEN
- ALTER TABLE storage_files
- ADD CONSTRAINT fk_storage_files_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_storage_files_folder') THEN
- ALTER TABLE storage_files
- ADD CONSTRAINT fk_storage_files_folder FOREIGN KEY (folder_id) REFERENCES storage_folders(id) ON DELETE SET NULL NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_receipt_templates_shop') THEN
- ALTER TABLE receipt_templates
- ADD CONSTRAINT fk_receipt_templates_shop FOREIGN KEY (shop_id) REFERENCES shops(id) NOT VALID;
- END IF;
-
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_products_price_nonnegative') THEN
- ALTER TABLE products
- ADD CONSTRAINT chk_products_price_nonnegative CHECK (price >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_quantity_nonnegative') THEN
- ALTER TABLE inventory_items
- ADD CONSTRAINT chk_inventory_items_quantity_nonnegative CHECK (quantity >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_reserved_quantity_nonnegative') THEN
- ALTER TABLE inventory_items
- ADD CONSTRAINT chk_inventory_items_reserved_quantity_nonnegative CHECK (reserved_quantity >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_reserved_not_above_quantity') THEN
- ALTER TABLE inventory_items
- ADD CONSTRAINT chk_inventory_items_reserved_not_above_quantity CHECK (reserved_quantity <= quantity) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_reorder_level_nonnegative') THEN
- ALTER TABLE inventory_items
- ADD CONSTRAINT chk_inventory_items_reorder_level_nonnegative CHECK (reorder_level >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_items_cost_per_unit_nonnegative') THEN
- ALTER TABLE inventory_items
- ADD CONSTRAINT chk_inventory_items_cost_per_unit_nonnegative CHECK (cost_per_unit >= 0) NOT VALID;
- END IF;
- ALTER TABLE inventory_transactions DROP CONSTRAINT IF EXISTS chk_inventory_transactions_quantity_nonnegative;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_transactions_quantity_nonzero') THEN
- ALTER TABLE inventory_transactions
- ADD CONSTRAINT chk_inventory_transactions_quantity_nonzero CHECK (quantity <> 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_inventory_transactions_unit_cost_nonnegative') THEN
- ALTER TABLE inventory_transactions
- ADD CONSTRAINT chk_inventory_transactions_unit_cost_nonnegative CHECK (unit_cost IS NULL OR unit_cost >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_tables_capacity_nonnegative') THEN
- ALTER TABLE tables
- ADD CONSTRAINT chk_tables_capacity_nonnegative CHECK (capacity >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_tables_hourly_rate_nonnegative') THEN
- ALTER TABLE tables
- ADD CONSTRAINT chk_tables_hourly_rate_nonnegative CHECK (hourly_rate >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_orders_amounts_nonnegative') THEN
- ALTER TABLE orders
- ADD CONSTRAINT chk_orders_amounts_nonnegative CHECK (
- total_amount >= 0
- AND discount_amount >= 0
- AND (amount_tendered IS NULL OR amount_tendered >= 0)
- AND (change_amount IS NULL OR change_amount >= 0)
- ) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_quantity_nonnegative') THEN
- ALTER TABLE order_items
- ADD CONSTRAINT chk_order_items_quantity_nonnegative CHECK (quantity >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_unit_price_nonnegative') THEN
- ALTER TABLE order_items
- ADD CONSTRAINT chk_order_items_unit_price_nonnegative CHECK (unit_price >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_payment_transactions_amounts_nonnegative') THEN
- ALTER TABLE payment_transactions
- ADD CONSTRAINT chk_payment_transactions_amounts_nonnegative CHECK (
- amount >= 0
- AND (amount_tendered IS NULL OR amount_tendered >= 0)
- AND change_amount >= 0
- ) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_returns_refund_amount_nonnegative') THEN
- ALTER TABLE order_returns
- ADD CONSTRAINT chk_order_returns_refund_amount_nonnegative CHECK (refund_amount >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_return_items_quantity_positive') THEN
- ALTER TABLE order_return_items
- ADD CONSTRAINT chk_order_return_items_quantity_positive CHECK (quantity > 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_return_items_unit_price_nonnegative') THEN
- ALTER TABLE order_return_items
- ADD CONSTRAINT chk_order_return_items_unit_price_nonnegative CHECK (unit_price >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_refund_transactions_amount_positive') THEN
- ALTER TABLE refund_transactions
- ADD CONSTRAINT chk_refund_transactions_amount_positive CHECK (amount > 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_wallets_balance_nonnegative') THEN
- ALTER TABLE wallets
- ADD CONSTRAINT chk_wallets_balance_nonnegative CHECK (balance >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_campaigns_amounts_nonnegative') THEN
- ALTER TABLE campaigns
- ADD CONSTRAINT chk_campaigns_amounts_nonnegative CHECK (
- face_value >= 0
- AND discount_value >= 0
- AND total_vouchers >= 0
- AND issued_vouchers >= 0
- ) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_campaigns_date_range') THEN
- ALTER TABLE campaigns
- ADD CONSTRAINT chk_campaigns_date_range CHECK (start_date IS NULL OR end_date IS NULL OR end_date >= start_date) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_vouchers_discount_value_nonnegative') THEN
- ALTER TABLE vouchers
- ADD CONSTRAINT chk_vouchers_discount_value_nonnegative CHECK (discount_value >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_attendance_records_checkout_after_checkin') THEN
- ALTER TABLE attendance_records
- ADD CONSTRAINT chk_attendance_records_checkout_after_checkin CHECK (check_out_at IS NULL OR check_out_at >= check_in_at) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_storage_files_byte_size_nonnegative') THEN
- ALTER TABLE storage_files
- ADD CONSTRAINT chk_storage_files_byte_size_nonnegative CHECK (byte_size >= 0) NOT VALID;
- END IF;
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_receipt_templates_tax_rate_nonnegative') THEN
- ALTER TABLE receipt_templates
- ADD CONSTRAINT chk_receipt_templates_tax_rate_nonnegative CHECK (tax_rate >= 0) NOT VALID;
- END IF;
- END
- $integrity_constraints$;
-
- INSERT INTO mvp_roles (id, code, name, portal) VALUES
- (gen_random_uuid(), 'superadmin', 'Super Admin', 'superadmin'),
- (gen_random_uuid(), 'admin', 'Quản trị cửa hàng', 'admin'),
- (gen_random_uuid(), 'staff', 'Nhân viên', 'staff'),
- (gen_random_uuid(), 'customer', 'Khách hàng', 'customer')
- ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, portal = EXCLUDED.portal;
-
- INSERT INTO platform_plans (id, code, name, price, features) VALUES
- (gen_random_uuid(), 'starter', 'Starter', 0, '["POS", "Catalog", "Inventory"]'::jsonb),
- (gen_random_uuid(), 'growth', 'Growth', 299000, '["Multi-shop", "Staff", "Reports", "AI"]'::jsonb),
- (gen_random_uuid(), 'scale', 'Scale', 799000, '["Super admin", "Marketing", "Storage", "Integrations"]'::jsonb)
- ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, price = EXCLUDED.price, features = EXCLUDED.features;
-
- INSERT INTO feature_flags (key, description, enabled) VALUES
- ('ai_chat', 'AI shop assistant with tool calling', true),
- ('external_storage', 'S3-compatible file storage', true),
- ('social_marketing', 'Social publishing adapters', true),
- ('customer_qr_ordering', 'Customer QR menu ordering', true)
- ON CONFLICT (key) DO UPDATE SET description = EXCLUDED.description, enabled = EXCLUDED.enabled, updated_at = now();
- `);
+ await pool.query(coreSchemaSql);
}
diff --git a/microservices/apps/tpos-mvp-next/src/server/integrations/external.ts b/microservices/apps/tpos-mvp-next/src/server/integrations/external.ts
index a969a20d..20073497 100644
--- a/microservices/apps/tpos-mvp-next/src/server/integrations/external.ts
+++ b/microservices/apps/tpos-mvp-next/src/server/integrations/external.ts
@@ -4,6 +4,7 @@ type JsonRecord = Record
;
type AiCallOptions = {
model?: string | null;
systemPrompt?: string | null;
+ baseUrl?: string | null;
};
function requireEnv(name: string) {
@@ -43,8 +44,9 @@ async function postJson(url: string, body: unknown, headers: Record Boolean(value));
const canonical = [
"PUT",
path,
- accessLevel === "public" ? "x-amz-acl=public-read" : "",
- `host:${host}`,
- `x-amz-content-sha256:${payloadHash}`,
- `x-amz-date:${now}`,
+ "",
+ ...canonicalHeaders,
"",
signedHeaders,
payloadHash
@@ -164,10 +173,11 @@ export async function uploadS3Object(key: string, file: File, accessLevel = "pub
const scope = `${date}/${region}/s3/aws4_request`;
const stringToSign = ["AWS4-HMAC-SHA256", now, scope, sha256Hex(canonical)].join("\n");
const signature = hmacHex(signingKey(secretKey, date, region, "s3"), stringToSign);
- const response = await fetch(url + (accessLevel === "public" ? "?x-amz-acl=public-read" : ""), {
+ const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": file.type || "application/octet-stream",
+ ...(acl ? { "x-amz-acl": acl } : {}),
"x-amz-content-sha256": payloadHash,
"x-amz-date": now,
Authorization: `AWS4-HMAC-SHA256 Credential=${accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
@@ -230,19 +240,72 @@ export async function deleteS3Object(key: string) {
}
}
+function missingEnv(names: string[]) {
+ return names.filter((name) => !process.env[name]);
+}
+
+export function providerCredentialDetails() {
+ const groups = {
+ openai: ["OPENAI_API_KEY"],
+ anthropic: ["ANTHROPIC_API_KEY"],
+ openrouter: ["OPENROUTER_API_KEY"],
+ s3: ["S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"],
+ facebook: ["FACEBOOK_PAGE_TOKEN", "FACEBOOK_PAGE_ID"],
+ zalo: ["ZALO_OA_ACCESS_TOKEN"],
+ whatsapp: ["WHATSAPP_ACCESS_TOKEN", "WHATSAPP_PHONE_NUMBER_ID"],
+ x: ["X_API_KEY", "X_API_SECRET", "X_ACCESS_TOKEN", "X_ACCESS_SECRET"]
+ } as const;
+ return Object.fromEntries(
+ Object.entries(groups).map(([provider, names]) => {
+ const missing = missingEnv([...names]);
+ return [provider, { configured: missing.length === 0, missing }];
+ })
+ ) as Record;
+}
+
export function providerCredentialStatus() {
+ const details = providerCredentialDetails();
return {
- openai: Boolean(process.env.OPENAI_API_KEY),
- anthropic: Boolean(process.env.ANTHROPIC_API_KEY),
- openrouter: Boolean(process.env.OPENROUTER_API_KEY),
- s3: Boolean(process.env.S3_ENDPOINT && process.env.S3_REGION && process.env.S3_BUCKET && process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY),
- facebook: Boolean(process.env.FACEBOOK_PAGE_TOKEN && process.env.FACEBOOK_PAGE_ID),
- zalo: Boolean(process.env.ZALO_OA_ACCESS_TOKEN),
- whatsapp: Boolean(process.env.WHATSAPP_ACCESS_TOKEN && process.env.WHATSAPP_PHONE_NUMBER_ID),
+ openai: details.openai.configured,
+ anthropic: details.anthropic.configured,
+ openrouter: details.openrouter.configured,
+ s3: details.s3.configured,
+ facebook: details.facebook.configured,
+ zalo: details.zalo.configured,
+ whatsapp: details.whatsapp.configured,
x: false
};
}
+export function socialProviderCredentialStatus(provider: string) {
+ const normalized = provider.toLowerCase();
+ if (!["facebook", "zalo", "whatsapp", "x"].includes(normalized)) {
+ return { configured: false, missing: [], message: `Unsupported social provider: ${provider}` };
+ }
+ if (normalized === "x") {
+ return { configured: false, missing: [], message: "X publishing requires a dedicated OAuth 1.0a adapter before it can be enabled." };
+ }
+ const details = providerCredentialDetails();
+ const item = details[normalized as keyof ReturnType];
+ if (!item) return { configured: false, missing: [], message: `Unsupported social provider: ${provider}` };
+ return item.configured
+ ? { configured: true, missing: [], message: null }
+ : { configured: false, missing: item.missing, message: `${item.missing.join(", ")} required for ${provider} publishing` };
+}
+
+export function aiProviderCredentialStatus(provider: string) {
+ const normalized = provider.toLowerCase();
+ const mapped = normalized === "claude" ? "anthropic" : normalized;
+ if (!["openai", "openrouter", "anthropic"].includes(mapped)) {
+ return { configured: false, missing: [], message: `Unsupported AI provider: ${provider}` };
+ }
+ const details = providerCredentialDetails();
+ const item = details[mapped as "openai" | "openrouter" | "anthropic"];
+ return item.configured
+ ? { configured: true, missing: [], message: null }
+ : { configured: false, missing: item.missing, message: `${item.missing.join(", ")} required for ${provider} AI chat` };
+}
+
export async function publishSocial(provider: string, input: JsonRecord) {
const content = stringValue(input.content);
if (!content) throw new Error("content is required");
diff --git a/microservices/apps/tpos-mvp-next/src/server/services/order.ts b/microservices/apps/tpos-mvp-next/src/server/services/order.ts
index fe3efe59..8530470b 100644
--- a/microservices/apps/tpos-mvp-next/src/server/services/order.ts
+++ b/microservices/apps/tpos-mvp-next/src/server/services/order.ts
@@ -8,7 +8,8 @@ import {
listOrders,
listOrdersPaged,
payOrder,
- returnOrder
+ returnOrder,
+ transferOrderTable
} from "../db/queries";
import type { OrderFilters, PagedOrderResult } from "../domain/types";
@@ -98,6 +99,13 @@ export async function returnOrderService(
return returnOrder(orderId, input);
}
+export async function transferOrderTableService(
+ orderId: string,
+ input: Parameters[1]
+) {
+ return transferOrderTable(orderId, input);
+}
+
export async function createPosOrder(input: OrderCreateInput) {
return createOrderService(input);
}
diff --git a/microservices/apps/tpos-mvp-next/src/server/services/parity.ts b/microservices/apps/tpos-mvp-next/src/server/services/parity.ts
index c75d751e..0b5b654c 100644
--- a/microservices/apps/tpos-mvp-next/src/server/services/parity.ts
+++ b/microservices/apps/tpos-mvp-next/src/server/services/parity.ts
@@ -89,6 +89,7 @@ export async function seedParityData(defaultShopId?: string) {
const users = [
["admin@goodgo.vn", "Admin@123", "Quản trị GoodGo", "admin"],
["staff@goodgo.vn", "Staff@123", "Nhân viên POS", "staff"],
+ ["marketing@goodgo.vn", "Marketing@123", "Marketing GoodGo", "marketing"],
["customer@goodgo.vn", "Customer@123", "Khách hàng mẫu", "customer"],
["superadmin@goodgo.vn", "SuperAdmin@123", "Super Admin", "superadmin"]
] as const;
@@ -101,7 +102,7 @@ export async function seedParityData(defaultShopId?: string) {
ON CONFLICT (email) DO UPDATE
SET display_name = EXCLUDED.display_name,
password_hash = EXCLUDED.password_hash,
- default_shop_id = EXCLUDED.default_shop_id,
+ default_shop_id = COALESCE(mvp_users.default_shop_id, EXCLUDED.default_shop_id),
updated_at = now()`,
[userId, email, hashPassword(password), displayName, roleCode === "superadmin" ? null : shop.id]
);
@@ -182,11 +183,12 @@ export async function seedParityData(defaultShopId?: string) {
[shop.id]
);
if (campaign[0]) {
+ const sampleVoucherCode = `GG-${slugify(shop.slug || shop.name || shop.id).toUpperCase()}-20K`;
await query(
`INSERT INTO vouchers (id, campaign_id, shop_id, code, discount_type, discount_value)
- VALUES ($1, $2, $3, 'GG-MVP-20K', $4, $5)
+ VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (code) DO NOTHING`,
- [randomUUID(), campaign[0].id, shop.id, campaign[0].discount_type, campaign[0].discount_value]
+ [randomUUID(), campaign[0].id, shop.id, sampleVoucherCode, campaign[0].discount_type, campaign[0].discount_value]
);
}
@@ -1524,6 +1526,9 @@ export async function getAiConfig(shopId: string) {
export async function saveAiConfig(input: JsonRecord) {
const shopId = stringValue(input.shopId);
if (!shopId) throw new Error("shopId is required");
+ if (stringValue(input.apiKey)) {
+ throw new Error("Raw apiKey values are not stored in MVP. Configure provider env credentials or pass apiKeyRef from a secret manager.");
+ }
await query(
`INSERT INTO ai_configs (shop_id, provider, api_key_ref, model, base_url, system_prompt, enabled)
VALUES ($1, $2, $3, $4, $5, $6, $7)
@@ -1536,9 +1541,9 @@ export async function saveAiConfig(input: JsonRecord) {
enabled = EXCLUDED.enabled,
updated_at = now()`,
[
- shopId,
- stringValue(input.provider) ?? process.env.AI_DEFAULT_PROVIDER ?? "openai",
- stringValue(input.apiKeyRef) ?? stringValue(input.apiKey),
+ shopId,
+ stringValue(input.provider) ?? process.env.AI_DEFAULT_PROVIDER ?? "openai",
+ stringValue(input.apiKeyRef),
stringValue(input.model) ?? "gpt-5.1",
stringValue(input.baseUrl),
stringValue(input.systemPrompt),
@@ -1721,7 +1726,7 @@ export async function publicShop(shopId: string) {
export async function publicMenu(shopId: string) {
const shop = await getPublicShop(shopId);
if (!shop) throw new Error("Shop not found");
- const [categories, products] = await Promise.all([listCategories(shopId), listProducts(shopId)]);
+ const [categories, products] = await Promise.all([listCategories(shop.id), listProducts(shop.id)]);
const grouped = categories.map((category) => ({
...category,
items: products.filter((product) => product.categoryId === category.id)