-- Meshkee CMS — shopping cart and orders -- --------------------------------------------------------------------------- -- enums -- --------------------------------------------------------------------------- DO $$ BEGIN CREATE TYPE order_status AS ENUM ( 'pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE order_source AS ENUM ('website', 'admin'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- --------------------------------------------------------------------------- -- carts (one per customer per business) -- --------------------------------------------------------------------------- CREATE TABLE carts ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, business_id BIGINT NOT NULL, user_id BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT carts_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE, CONSTRAINT carts_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, CONSTRAINT carts_business_user_unique UNIQUE (business_id, user_id) ); CREATE INDEX idx_carts_business_id ON carts (business_id); CREATE INDEX idx_carts_user_id ON carts (user_id); CREATE TRIGGER carts_set_updated_at BEFORE UPDATE ON carts FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- --------------------------------------------------------------------------- -- cart items (product variants in cart) -- --------------------------------------------------------------------------- CREATE TABLE cart_items ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, cart_id BIGINT NOT NULL, variant_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT cart_items_cart_id_fkey FOREIGN KEY (cart_id) REFERENCES carts (id) ON DELETE CASCADE, CONSTRAINT cart_items_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES product_variants (id) ON DELETE CASCADE, CONSTRAINT cart_items_cart_variant_unique UNIQUE (cart_id, variant_id), CONSTRAINT cart_items_quantity_positive CHECK (quantity > 0) ); CREATE INDEX idx_cart_items_cart_id ON cart_items (cart_id); CREATE INDEX idx_cart_items_variant_id ON cart_items (variant_id); CREATE TRIGGER cart_items_set_updated_at BEFORE UPDATE ON cart_items FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- --------------------------------------------------------------------------- -- orders -- --------------------------------------------------------------------------- CREATE TABLE orders ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, business_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_number VARCHAR(30) NOT NULL, status order_status NOT NULL DEFAULT 'pending', source order_source NOT NULL DEFAULT 'website', subtotal NUMERIC(12, 2) NOT NULL DEFAULT 0, shipping_total NUMERIC(12, 2) NOT NULL DEFAULT 0, discount_total NUMERIC(12, 2) NOT NULL DEFAULT 0, total NUMERIC(12, 2) NOT NULL DEFAULT 0, shipping_address JSONB NOT NULL DEFAULT '{}', address_id BIGINT, customer_notes TEXT, admin_notes TEXT, created_by BIGINT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT orders_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE, CONSTRAINT orders_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE RESTRICT, CONSTRAINT orders_address_id_fkey FOREIGN KEY (address_id) REFERENCES addresses (id) ON DELETE SET NULL, CONSTRAINT orders_created_by_fkey FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL, CONSTRAINT orders_business_order_number_unique UNIQUE (business_id, order_number), CONSTRAINT orders_subtotal_non_negative CHECK (subtotal >= 0), CONSTRAINT orders_shipping_total_non_negative CHECK (shipping_total >= 0), CONSTRAINT orders_discount_total_non_negative CHECK (discount_total >= 0), CONSTRAINT orders_total_non_negative CHECK (total >= 0) ); CREATE INDEX idx_orders_business_created ON orders (business_id, created_at DESC); CREATE INDEX idx_orders_business_user ON orders (business_id, user_id, created_at DESC); CREATE INDEX idx_orders_business_status ON orders (business_id, status, created_at DESC); CREATE TRIGGER orders_set_updated_at BEFORE UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- --------------------------------------------------------------------------- -- order items (line items with price snapshots) -- --------------------------------------------------------------------------- CREATE TABLE order_items ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, order_id BIGINT NOT NULL, variant_id BIGINT, product_id BIGINT NOT NULL, product_title VARCHAR(255) NOT NULL, variant_sku VARCHAR(100), unit_price NUMERIC(12, 2) NOT NULL, compare_at_price NUMERIC(12, 2), quantity INT NOT NULL, line_total NUMERIC(12, 2) NOT NULL, selections_snapshot JSONB NOT NULL DEFAULT '[]', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE, CONSTRAINT order_items_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES product_variants (id) ON DELETE SET NULL, CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT, CONSTRAINT order_items_quantity_positive CHECK (quantity > 0), CONSTRAINT order_items_unit_price_non_negative CHECK (unit_price >= 0), CONSTRAINT order_items_line_total_non_negative CHECK (line_total >= 0) ); CREATE INDEX idx_order_items_order_id ON order_items (order_id); CREATE INDEX idx_order_items_variant_id ON order_items (variant_id); -- --------------------------------------------------------------------------- -- permissions -- --------------------------------------------------------------------------- INSERT INTO permissions (name, slug, group_name, description) VALUES ('Create orders', 'orders.create', 'orders', 'Create orders on behalf of customers'), ('Update orders', 'orders.update', 'orders', 'Update order status and admin notes') ON CONFLICT (slug) DO NOTHING; -- business_owner INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.slug IN ('orders.read', 'orders.create', 'orders.update') WHERE r.slug = 'business_owner' ON CONFLICT DO NOTHING; -- owner (legacy global role) INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.slug IN ('orders.read', 'orders.create', 'orders.update') WHERE r.slug = 'owner' ON CONFLICT DO NOTHING; -- admin INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.slug IN ('orders.read', 'orders.create', 'orders.update') WHERE r.slug = 'admin' ON CONFLICT DO NOTHING; -- editor: read + update status INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.slug IN ('orders.read', 'orders.update') WHERE r.slug = 'editor' ON CONFLICT DO NOTHING; -- viewer: read only INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.slug = 'orders.read' WHERE r.slug = 'viewer' ON CONFLICT DO NOTHING;