Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save isocroft/177bb3e5823a61c3ec7eda701eedb575 to your computer and use it in GitHub Desktop.

Select an option

Save isocroft/177bb3e5823a61c3ec7eda701eedb575 to your computer and use it in GitHub Desktop.
A database schema for an single-DB_shared-schema multi-tenant SaaS e-commerce storefront with inventory+backoffice that specializes in selling apparels and ready-made clothing as well as smartphones using either MySQL, SQLite or PostgreSQL as primary database
-- MySQL v8.0.16
-- PostgresSQL v16.9.2
CREATE DATABASE IF NOT EXISTS `test`
DEFAULT CHARACTER SET utf8 -- utf8mb4
DEFAULT COLLATE utf8_general_ci; -- utf8mb4_unicode_ci
SET default_storage_engine = INNODB;
CREATE TABLE organizations (
id bigint NOT NULL,
company_name varchar(150) NOT NULL,
company_logo_url varchar(130), -- start with value of NULL and use background job to upload image to `cloudinary` OR `aws-s3` an then later write url value back
industry enum('education', 'financial-services', 'legal', 'manufacturing', 'health', 'food-processing', 'agro-materials', 'fashion-design + tailoring') NOT NULL,
company_type enum('sole-proprietor_SME', 'corporation_STARTUP', 'sole-proprietor_SB', 'corporation_BIG_FIRM'),
registration_number char(11) NOT NULL,
registration_country_code enum('NG', 'US', 'KE', 'GB') NOT NULL,
verified_by enum('premble', 'ondato', 'idenfy', 'verifyme') NOT NULL,
verified_at datetime NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE organization_teams (
id bigint NOT NULL,
tenant_id bigint NOT NULL,
email varchar(50) NOT NULL UNIQUE,
role enum('manager', 'assistant', 'support') NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE organization_team_accounts (
id bigint NOT NULL,
email varchar(50) NOT NULL UNIQUE, -- data redundancy for easier access
password varchar(36) NOT NULL,
profile_avatar_url varchar(130), -- start with value of NULL and use background job to upload image to `cloudinary` OR `aws-s3` an then later write url value back
tenant_id bigint NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (id) REFERENCES organization_teams (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE organization_team_roles (
id bigint NOT NULL,
role_name varchar(50) UNIQUE NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE organization_team_permissions (
id bigint NOT NULL,
permission_name varchar(100) UNIQUE NOT NULL,
description text,
PRIMARY KEY (id)
);
CREATE TABLE organization_team_account_roles (
account_id bigint NOT NULL,
role_id bigint NOT NULL,
PRIMARY KEY (account_id, role_id),
FOREIGN KEY (account_id) REFERENCES organization_team_accounts (id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES organization_team_roles (id) ON DELETE CASCADE
);
CREATE TABLE organization_team_role_permissions (
role_id bigint NOT NULL,
permission_id bigint NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES organization_team_roles (id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES organization_team_permissions (id) ON DELETE CASCADE
);
CREATE TABLE shoppers (
id bigint NOT NULL,
first_name varchar(100) NOT NULL,
last_name varchar(100) NOT NULL,
profile_avatar_url varchar(130), -- start with value of NULL and use background job to upload image to `cloudinary` OR `aws-s3` an then later write url value back
gender enum('male', 'female') NOT NULL,
date_of_birth date NOT NULL,
registered_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT riderunq UNIQUE (id, first_name, last_name, date_of_birth),
PRIMARY KEY (id)
);
CREATE TABLE shopper_accounts (
id bigint NOT NULL,
primary_email varchar(50) NOT NULL, -- It is possible to have the same shopper shopping at 2 or more diferent tenants
full_name varchar(150) NOT NULL, -- data redundancy for easier access
password varchar(36) NOT NULL,
last_login_at datetime,
is_active tinyint(1) NOT NULL DEFAULT 1,
email_verified tinyint(1) NOT NULL DEFAULT 0,
tenant_id bigint NOT NULL,
phone_number varchar(16) UNIQUE NULL,
referral_shareable_code char(9) NOT NULL,
referrals_count int NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE customer_emails (
id bignint NOT NULL,
email varchar(50) NOT NULL UNIQUE,
is_primary tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
-- Pivot table
CREATE TABLE shopper_emails (
email_id bignint NOT NULL,
tenant_id bigint NOT NULL,
shopper_id bignint NOT NULL,
PRIMARY KEY (shopper_id, email_id),
FOREIGN KEY (shopper_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE,
FOREIGN KEY (email_id) REFERENCES customer_emails (id) ON DELETE CASCADE
);
-- System table (doesn't have a `tenant_id` column because this table is populated by the SaaS provider and use by all tenants)
CREATE TABLE world_address_countries (
id bigint NOT NULL,
name char('2') NOT NULL,
PRIMARY KEY (id)
);
-- System table (doesn't have a `tenant_id` column because this table is populated by the SaaS provider and use by all tenants)
CREATE TABLE world_address_states (
id bigint NOT NULL,
name char('2') NOT NULL,
country_id bigint NOT NULL,
country_name char('2') NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (country_id) REFERENCES world_address_countries (id) ON DELETE CASCADE
);
-- System table (doesn't have a `tenant_id` column because this table is populated by the SaaS provider and use by all tenants)
CREATE TABLE world_location_addresses (
id bigint NOT NULL,
street_name varchar(150) NOT NULL,
house_number char(6),
landmarks json NOT NULL DEFAULT (JSON_ARRAY()),
location_latlng point,
city_town varchar(60) NOT NULL,
state_id bigint NOT NULL,
zipcode char(9),
country_id bigint NOT NULL,
device_id varchar(110) UNIQUE NULL, -- either a moblie device id or a browser fingerprint or NULL
PRIMARY KEY (id),
FOREIGN KEY (state_id) REFERENCES world_address_states (id) ON DELETE CASCADE,
FOREIGN KEY (country_id) REFERENCES world_address_countries (id) ON DELETE CASCADE
);
-- Pivot table
CREATE TABLE shopper_shipping_addresses (
shopper_id bigint NOT NULL,
tenant_id bigint NOT NULL,
world_location_address_id NOT NULL,
PRIMARY KEY (shopper_id, world_location_address_id),
FOREIGN KEY (shopper_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE,
FOREIGN KEY (world_location_address_id) REFERENCES world_location_addresses (id) ON DELETE CASCADE
);
CREATE TABLE tenant_office_addresses (
tenant_id bigint NOT NULL,
world_location_address_id NOT NULL,
PRIMARY KEY (tenant, world_location_address_id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE,
FOREIGN KEY (world_location_address_id) REFERENCES world_location_addresses (id) ON DELETE CASCADE
);
CREATE TABLE referral_rules (
id bigint NOT NULL,
rule_valid_from_time datetime NOT NULL,
rule_valid_to_time datetime NOT NULL,
credit_for_referrer_user int,
credit_for_referred_user int,
credit_kickback_to_referrer number,
credit_kickback_referred_spend_minimum number,
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
-- Pivot table
CREATE TABLE referral_manifest (
referrer_user_id bigint NOT NULL,
referred_user_id bigint NOT NULL,
used_at datetime NOT NULL,
tenant_id bigint NOT NULL,
rule_id bigint NOT NULL,
PRIMARY KEY (referrer_user_id, referred_user_id),
FOREIGN KEY (referrer_user_id) REFERENCES shopper_accounts (id) ON DELETE CASCADE,
FOREIGN KEY (referred_user_id) REFERENCES shopper_accounts (id) ON DELETE CASCADE,
FOREIGN KEY (rule_id) REFERENCES referral_rules (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE shopping_carts (
id bigint NOT NULL,
cart_reference varchar(80) NOT NULL UNIQUE, -- cuid v2 ids
shopper_id bigint, -- NULL if guest user else logged-in user id
status enum('picked', 'in-use', 'abandoned', 'returned') NOT NULL DEFAULT 'picked', -- all shopping carts will be deleted after 48hrs from `created_at` if they still have an 'picked' status. The shopping cart status is set to 'returned' after you pay for the order associated with the cart
tenant_id bigint NOT NULL,
device_id varchar(110) NOT NULL UNIQUE, -- either a mobile device id or a browser fingerprint (this goes into a HTTP cookie that lasts 48hrs from `created_at`)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (shopper_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE orders (
id bigint NOT NULL,
customer_id bigint NOT NULL,
tenant_id bigint NOT NULL,
checkout_status_started tinyint(1) NOT NULL DEFAULT 1,
checkout_status_completed tinyint(1) NOT NULL DEFAULT 0,
checkout_status_abandoned tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
FOREIGN KEY (customer_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE order_invoices (
id bigint NOT NULL,
payer_id bigint NOT NULL,
tenant_id bigint NOT NULL,
total_fee_amount_paid decimal(10, 2) NOT NULL DEFAULT 0.00, -- will only be populated when the shopper actually pays
total_tax_amount_paid decimal(10, 2) NOT NULL DEFAULT 0.00, -- will only be populated when the shopper actually pays
due_date date NOT NULL, -- `due_date` is set 4 days from `created_at`
note text,
-- example `order_items`: { "item_id_[`product_stock_id`]": { "quantity": 1, "item_total": 62000.00, "product_variant_id": 2222 } }
order_items json NOT NULL DEFAULT (JSON_OBJECT()), -- `order_items` is an empty JSON object intially until after payment for the order has been made then we populate it with cart items with their `quantity` and `item_total`
payment_status_paid tinyint(1) NOT NULL DEFAULT 0,
payment_status_unpaid tinyint(1) NOT NULL DEFAULT 1,
payment_status_retried tinyint(1) NOT NULL DEFAULT 0,
payment_status_failed tintyint(1) NOT NULL DEFAULT 0,
created_at date NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (payer_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
-- Invoice Statuses:
-- =================
-- 1. It can be 'unpaid' & 'failed'; meaning the shopper triedd to pay before but failed while not past the due date
-- 2. It can be 'overdue' & 'failed'; meaning the shopper tried to pay before but failed and its past the due date
-- 3. It can be 'paid' & 'retried'; meaning the shopper tried more than once to pay before succeeding while not past the due date
ALTER TABLE order_invoices ADD CONSTRAINT chk_order_invoices_pay_stats_paid CHECK (payment_status_unpaid <> 1 AND (payment_status_paid = 0 OR payment_status_paid = 1));
ALTER TABLE order_invoices ADD CONSTRAINT chk_order_invoices_pay_stats_retried CHECK (payment_status_failed <> 1 AND (payment_status_retried = 0 OR payment_status_retried = 1));
ALTER TABLE order_invoices ADD CONSTRAINT chk_order_invoices_pay_stats_paid_retried CHECK (payment_status_paid = 1 AND (payment_status_retried = 0 OR payment_status_retried = 1));
ALTER TABLE order_invoices ADD CONSTRAINT chk_order_invoices_pay_stats_unpaid_failed CHECK (payment_status_unpaid = 1 AND payment_status_paid = 0 AND payment_status_failed = 1);
ALTER TABLE order_invoices ADD CONSTRAINT chk_order_invoices_pay_stats_overdue_paid CHECK (due_date < CURRENT_DATE() AND payment_status_paid = 1 AND payment_status_unpaid = 0);
-- System table (doesn't have a `tenant_id` column because this table is populated by the SaaS provider and use by all tenants)
CREATE TABLE order_shipment_transit_delivery_methods (
id bigint NOT NULL,
transit_option enum('bike', 'truck', 'van', 'bicycle', 'shipping_vessel') NOT NULL,
transit_type enum('last-mile', 'middle-mile', 'white-glove') NOT NULL,
transit_period enum('same-day', 'express-overnight', 'standard') NOT NULL,
PRIMARY KEY (id)
);
-- System table (doesn't have a `tenant_id` column because this table is populated by the SaaS provider and use by all tenants)
CREATE TABLE order_shipment_transit_carriers (
id bigint NOT NULL,
carrier_name varchar(70) NOT NULL, -- e.g. FedEx, UPS, USPS, DHL, KwikDelivery
carrier_foul_up_rate char(4) NOT NULL DEFAULT '0%',
PRIMARY KEY (id)
);
-- Pivot table
CREATE TABLE order_shipment_transit_carrier_delivery_methods (
carried_id bigint NOT NULL,
delivery_method_id bigint NOT NULL,
PRIMARY KEY (carried_id, delivery_method_id),
FOREIGN KEY (carrier_id) REFERENCES order_shipment_transit_carriers (id) ON DELETE CASCADE,
FOREIGN KEY (delivery_method_id) REFERENCES order_shipment_transit_delivery_methods (id) ON DELETE CASCADE
);
-- System table (doesn't have a `tenant_id` column because this table is populated by the SaaS provider and use by all tenants)
CREATE TABLE order_shipment_transit_carrier_stops (
id bigint NOT NULL,
carrier_id bigint NOT NULL,
type enum('consignor_drop-off', 'transit_drop-off') NOT NULL,
stop_location_address text,
PRIMARY KEY (id),
FOREIGN KEY (carrier_id) REFERENCES order_shipment_transit_carriers (id) ON DELETE CASCADE
);
CREATE TABLE installed_order_shipment_transit_carriers (
id bigint NOT NULL,
tenant_id bigint NOT NULL,
carried_id bigint NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (carrier_id) REFERENCES order_shipment_transit_carriers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE order_shipping_log (
id bigint NOT NULL,
total_fee_amount_paid decimal(10, 2) NOT NULL DEFAULT 0.00, -- will only be populated when the shopper actually pays
total_tax_amount_paid decimal(10, 2) NOT NULL DEFAULT 0.00, -- will only be populated when the shopper actually pays
tenant_id bigint NOT NULL,
consignee_id bigint NOT NULL,
tracking_number varchar(100), -- The tracking number is unknown and only generated after payment for the order has been made hence it is NULL initially
shipment_status_shipped tinyint(1) NOT NULL DEFAULT 0,
shipment_status_in_transit tinyint(1) NOT NULL DEFAULT 0,
shipment_status_delivered tinyint(1) NOT NULL DEFAULT 0,
shipment_status_delayed tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
FOREIGN KEY (consignee_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
-- Shipment Statuses:
-- ==================
-- 1. It can be 'shipped' & 'in-transit'; meaning the shipment has left the fulfilment center and is on the move to the shopper for delivery
-- 2. It can be 'shipped' & 'delayed'; meaning the shipment has left the fufilment center but is stuck somewhere and not on the move for delivery
-- 3. It can be 'shipped' & 'delivered'; meaning the shipment has left the fufilment center and has reached its' destination and delivered to the shopper
ALTER TABLE order_shipping_log ADD CONSTRAINT chk_order_shipping_log_ship_stats_delivrd CHECK (shipment_status_delayed <> 1 AND (shipment_status_delivered = 1 OR shipment_status_delivered = 0));
ALTER TABLE order_shipping_log ADD CONSTRAINT chk_order_shipping_log_ship_stats_intrnst CHECK (shipment_status_delivered <> 1 AND (shipment_status_in_transit = 1 OR shipment_status_in_transit = 0));
ALTER TABLE order_shipping_log ADD CONSTRAINT chk_order_shipping_log_ship_stats_delayd CHECK (shipment_status_in_transit <> 1 AND (shipment_status_delayed = 0 OR shipment_status_delayed = 1));
ALTER TABLE order_shipping_log ADD CONSTRAINT chk_order_shipping_log_ship_stats_intrnst_shipd CHECK (shipment_status_in_transit = 1 AND shipment_status_delayed = 0 AND shipment_status_shipped = 1);
ALTER TABLE order_shipping_log ADD CONSTRAINT chk_order_shipping_log_ship_stats_delivrd_shipd CHECK (shipment_status_delivered = 1 AND shipment_status_in_transit = 0 AND shipment_status_shipped = 1);
ALTER TABLE order_shipping_log ADD CONSTRAINT chk_order_shipping_log_ship_stats_delayd_shipd CHECK (shipment_status_delayed = 1 AND (shipment_status_shipped = 0 OR shipment_status_shipped = 1));
CREATE TABLE order_shipment_transit_manifest (
shipping_log_id bigint NOT NULL,
shipment_pickup_date datetime NOT NULL,
shipment_dropoff_date datetime NOT NULL,
shipment_carrier_ingress_stop_id bigint NOT NULL,
shipment_carrier_id bigint NOT NULL,
tenant_id bigint NOT NULL,
shipment_transit_delivery_method_id bigint NOT NULL,
shipment_carrier_stop_ordering enum('first', 'intermediate', 'last') NOT NULL,
requires_consignee_dropoff tinyint(1) NOT NULL DEFAULT 0,
is_consignee_dropoff tinyint(1) NOT NULL DEFAULT 0,
shipment_carrier_stop_status_details json NOT NULL DEFAULT (JSON_OBJECT()),
PRIMARY KEY (shipping_log_id, shipment_carrier_id, shipment_carrier_ingress_stop_id),
FOREIGN KEY (shipping_log_id) REFERENCES order_shipping_log (id) ON DELETE CASCADE,
FOREIGN KEY (shipment_carrier_id) REFERENCES order_shipment_transit_carriers (id) ON DELETE NO ACTION,
FOREIGN KEY (shipment_carrier_ingress_stop_id) REFERENCES order_shipment_transit_carrier_stops (id) ON DELETE NO ACTION,
FOREIGN KEY (shipment_transit_delivery_method_id) REFERENCES order_shipment_transit_delivery_methods (id) ON DELETE NO ACTION,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
ALTER TABLE order_shipment_transit_manifest ADD CONSTRAINT chk_stop_ord_first_dropoff CHECK (shipment_carrier_stop_ordering = 'first' AND requires_consignee_dropoff = 0 AND is_consignee_dropoff = 0); -- The first stop cannot require consignee drop-off
ALTER TABLE order_shipment_transit_manifest ADD CONSTRAINT chk_stop_ord_intmd_dropoff CHECK (shipment_carrier_stop_ordering = 'intermediate' AND is_consignee_dropoff = 0); -- if it requires a consignee drop-off, then it has to be an intermediate stop!
ALTER TABLE order_shipment_transit_manifest ADD CONSTRAINT chk_stop_ord_intmd_dropoff CHECK (shipment_carrier_stop_ordering = 'last' AND requires_consignee_dropoff = 0); -- The last stop cannot require consignee drop-off
CREATE TABLE order_basket_transactions (
invoice_id bigint NOT NULL UNIQUE,
order_id bigint NOT NULL UNIQUE,
shopping_cart_id bigint NOT NULL UNIQUE,
payment_method enum('paypal', 'stripe', 'paystack', 'cash', 'bank-transfer', 'card', '-') NOT NULL DEFAULT '-',
pay_on_delivery tinyint(1) NOT NULL DEFAULT 0,
currency enum('NGN', 'USD', 'KSH', 'GBP') NOT NULL,
shipment_start_date datetime, -- The start date of the shipment is unknown until ater payment for the order has been made hence it is NULL initially
shipment_end_date datetime, -- The end date of the shipment is unknown until ater payment for the order has been made hence it is NULL initially
shipment_carrier_stops_count int, -- knowing how many stops we have for a shipment helps to caculate the shipment statuses over time
tenant_id bigint NOT NULL,
shipping_log_id bigint NOT NULL UNIQUE,
shopper_address_id bigint NOT NULL,
PRIMARY KEY (order_id, shopper_id, invoice_id),
FOREIGN KEY (invoice_id) REFERENCES order_invoices (id) ON DELETE NO ACTION,
FOREIGN KEY (shopper_address_id) REFERENCES world_location_addresses (id) ON DELETE NO ACTION,
FOREIGN KEY (shopper_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE,
FOREIGN KEY (shipping_log_id) REFERENCES order_shipping_log (id) ON DELETE CASCADE
);
ALTER TABLE order_basket_transactions ADD CONSTRAINT chk_pay_methd_on_delvry CHECK (pay_on_delivery = 1 AND (payment_method = 'cash' OR payment_method = 'bank-transfer' payment_method = 'card'));
CREATE TABLE inventory_product_stock_manufacturers (
id bigint NOT NULL,
name varchar(90) NOT NULL,
tenant_id bigint NOT NULL,
industry enum('electronics', 'upholstery', 'food_processing', 'fashion_wears') NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE inventory_products (
id bigint NOT NULL,
unit_name varchar(130) NOT NULL,
unit_description text,
tenant_id bigint NOT NULL,
unit_image_url varchar(190), -- start with NULL value and use a background job to upload image to `cloudinary` OR `aws-s3` then later write back url here
refundable_on_return tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
-- Pivot table
CREATE TABLE shopper_wishlists (
id bigint NOT NULL,
shopper_id bigint NOT NULL,
tenant_id bigint NOT NULL,
wishlist_name varchar(90) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (shopper_id) REFERENCES shoppers (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
-- Pivot table
CREATE TABLE shoppers_wishlist_items (
id bigint NOT NULL,
wishlist_id bigint NOT NULL,
product_id bigint NOT NULL,
tenant_id bigint NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (wishlist_id) REFERENCES shopper_wishlists (id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES inventory_products (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE,
UNIQUE (wishlist_id, product_id) -- ensures no duplicate products in a single wishlist
);
CREATE TABLE inventory_categories (
id bigint NOT NULL,
tenant_id bigint NOT NULL,
name varchar(160) NOT NULL,
description text,
parent_category_id bigint, -- for hierarchical categories (self-referencing foreign key)
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE,
FOREIGN KEY (parent_category_id) REFERENCES inventory_categories (id) ON DELETE NO ACTION,
);
-- Pivot table
CREATE TABLE inventory_catalog_product_categories (
product_id bigint NOT NULL,
category_id bigint NOT NULL,
tenant_id bigint NOT NULL,
PRIMARY KEY (product_id, category_id),
FOREIGN KEY (product_id) REFERENCES inventory_products (id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES inventory_categories (id) ON DELETE NO ACTION,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE inventory_product_stock_variants (
id bigint NOT NULL,
-- upc_format varchar(16), -- e.g. 'XXXXXX-XXXXX-X' (Universal Product Code)
sku_format varchar(210), -- e.g. '[Style]-[Color]-[Size]-[Material]-[Brand]-[Weight]-[Collection Month]-[Collection Year]' (Stock Keeping Unit)
varaint_attributes json NOT NULL DEFAULT (JSON_OBJECT()), -- e.g. '{ "sizes", ["xl", "l", "sm"], "colors": ["red", "pink"], "styles": ["jumpsuit", "pina-fore"], "brands": ["nhn_couture", "house_of_josh"], "materials": ["spandex", "cotton"], "units_of_measure": ["kg", "g"], "weights": ["130 grams", "70 grams"], "collection_months_plus_years": ["October 2024", "February 2025"] }'
brand_id bigint NOT NULL,
tenant_id bigint NOT NULL,
variant_image_urls json NOT NULL DEFAULT (JSON_ARRAY()), -- start with empty JSON array value and use a background job to upload images to `cloudinary` OR `aws-s3` then later write back urls here
category_id bigint NOT NULL, -- data redundancy for easy access purposes
PRIMARY KEY (id)
FOREIGN KEY (brand_id) REFERENCES inventory_product_stock_manufacturers (id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES inventory_categories (id) ON DELETE NO ACTION,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE inventory_product_stock_manifest (
id bigint NOT NULL,
stock_units_total_stored bigint NOT NULL DEFAULT 0, -- PN Counter modelling (from CRDTs)
stock_units_total_sold bigint NOT NULL DEFAULT 0, -- PN Counter modelling (from CRDTs)
unit_price_amount bigint NOT NULL DEFAULT 0, -- assume it's free hence price = 0
unit_discount_price_amount bigint NOT NULL DEFAULT 0, -- assume theres' no discount hence discount_price = 0
unit_price_currency enum('NGN-kobo', 'USD-cents', 'GBP-shillings') NOT NULL,
tenant_id bigint NOT NULL,
product_id bigint NOT NULL UNIQUE,
product_variant_id bigint NOT NULL UNIQUE,
stock_units_min_count_limit bigint NOT NULL DEFAULT 0,
stock_units_max_count_limit bigint NOT NULL,
unit_discount_price_amount_updated_at datetime NOT NULL,
stock_available_count_updated_at datetime NOT NULL, -- this column is updated only when either `stock_units_total_stored` or `stock_units_total_sold` column is updated
PRIMARY KEY (id),
FOREIGN KEY (product_id) REFERENCES inventory_products (id) ON DELETE CASCADE,
FOREIGN KEY (product_variant_id) REFERENCES inventory_product_stock_variants (id) ON DELETE NO ACTION,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
ALTER TABLE inventory_product_stock_manifest ADD CONSTRAINT chk_stock_stored_sold CHECK (stock_unit_total_sold < stock_unit_total_stored);
ALTER TABLE inventory_product_stock_manifest ADD CONSTRAINT chk_stock_min_max_limits CHECK (stock_units_min_count_limit <= stock_units_max_count_limit);
CREATE TABLE shopping_cart_items (
id bigint NOT NULL,
shopping_cart_id bigint NOT NULL,
product_stock_id bigint NOT NULL,
item_quantity int NOT NULL DEFAULT 0,
item_total decimal(10, 2) NOT NULL DEFAULT 0.00,
item_description text,
tenant_id bigint NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (shopping_cart_id) REFERENCES shopping_carts (id) ON DELETE CASCADE,
FOREIGN KEY (product_stock_id) REFERENCES inventory_product_stock_manifest (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE inventory_stocking_managers (
id bigint NOT NULL,
first_name varchar(90) NOT NULL,
last_name varchar(90) NOT NULL,
tenant_id bigint NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE inventory_product_stock_suppliers (
id bigint NOT NULL,
supplier_name varchar(95) NOT NULL,
contact_person_fullname varchar(182) NOT NULL,
tenant_id bigint NOT NULL,
contact_person_phone_number char(15) NOT NULL,
contact_person_email varchar(50) NOT NULL,
supplier_office_address_location json NOT NULL DEFAULT (JSON_OBJECT()), -- '{ "latitude": "XX", "longitude": "XX", "address": "xxxxx xxx" }'
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
-- Pivot table
CREATE TABLE inventory_stock_supply_catalog_listings (
supplier_id bigint NOT NULL,
product_id bigint NOT NULL,
category_id bigint NOT NULL, -- data redundancy for easier access
stocking_manager_id bigint NOT NULL,
restock_status_details json NOT NULL DEFAULT (JSON_OBJECT()),
stock_unit_supply_count bigint NOT NULL,
last_restock_date datetime NOT NULL,
fufillment_center_address_location json NOT NULL DEFAULT (JSON_OBJECT()), -- '{ "latitude": "XX", "longitude": "XX", "address": "xxxxx xxx" }'
tenant_id bigint NOT NULL,
PRIMARY KEY (product_id, supplier_id),
FOREIGN KEY (product_id) REFERENCES inventory_products (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE,
FOREIGN KEY (stocking_manager_id) REFERENCES inventory_stocking_managers (id) ON DELETE NO ACTION,
FOREIGN KEY (category_id) REFERENCES inventory_categories (id) ON DELETE NO ACTION
);
-- Pivot table
CREATE TABLE inventory_product_stock_movement_manifest (
id bigint NOT NULL,
product_id bigint NOT NULL,
movement_direction enum('in', 'out') NOT NULL,
quanity_change int NOT NULL,
tenant_id bigint NOT NULL,
movement_note text,
movement_date datetime NOT NULL,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (product_id) REFERENCES inventory_products (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
CREATE TABLE inventory_shipment_transit_delivery_methods (
id bigint NOT NULL,
transit_option enum('bike', 'truck', 'van', 'shipping_vessel') NOT NULL,
transit_type enum('first-mile', 'white-glove') NOT NULL,
transit_period enum('express-overnight', 'standard') NOT NULL,
tenant_id bigint NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (tenant_id) REFERENCES organizations (id) ON DELETE CASCADE
);
-- 1. organizations (Base Tenant)
INSERT INTO organizations (id, company_name, company_logo_url, industry, company_type, registration_number, registration_country_code, verified_by, verified_at) VALUES
(100, 'Fashion Hub Nigeria Ltd', NULL, 'fashion-design + tailoring', 'corporation_BIG_FIRM', 'NG123456789', 'NG', 'premble', NOW()),
(101, 'Agro-Seeds Global', NULL, 'agro-materials', 'corporation_STARTUP', 'US987654321', 'US', 'ondato', NOW());
-- 2. world_address_countries (System table - Depends on organizations)
INSERT INTO world_address_countries (id, name) VALUES
(200, 'NG'), -- Nigeria
(201, 'US'); -- USA
-- 3. world_address_states (System table - Depends on world_address_countries)
INSERT INTO world_address_states (id, country_id, name, country_name) VALUES
(300, 200, 'LG', 'NG'), -- Lagos, Nigeria
(301, 201, 'NY', 'US'), -- New York, USA
(302, 200, 'ABJ', 'NG'); -- Abuja, Nigeria
-- 4. world_location_addresses (System + Tenant table - Depends on world_address_states, world_address_countries)
INSERT INTO world_location_addresses (id, street_name, house_number, landmarks, location_latlng, city_town, state_id, zipcode, country_id, device_id) VALUES
(400, 'Victoria Island Street', '5A', '["Near Bank Plaza", "Next to Ifesinachi Park"]', ST_GeomFromText('POINT(6.5244 3.3792)'), 'Ikeja', 300, '700568', 200, 'mobile-dev-id-001'),
(401, 'Helsinki Avenue', '101', '["Off 12th & 6th"]', ST_GeomFromText('POINT(40.7128 -74.0060)'), 'Harlem', 301, '10001', 201, 'browser-fingerprint-999'),
(402, 'Adelabu Crescent', 'H-38', '["Off Laide Street"]', ST_GeomFromText('POINT(96.6109 -51.0145)'), 'Garki', 302, NULL, 200, NULL); -- most likely a shipping carrier address
-- 5. shoppers (No external FKs, but needed for accounts/transactions)
INSERT INTO shoppers (id, first_name, last_name, profile_avatar_url, gender, date_of_birth, registered_at) VALUES
(500, 'Jane', 'Doe', 'https://res.cloudinary.com/shopper_avatars/image/upload/w_200,h_200/sample_1.jpg', 'female', '1995-03-20', NOW()),
(501, 'John', 'Smith', NULL, 'male', '1988-11-15', NOW()),
(502, 'Amaka', 'Uke', 'https://res.cloudinary.com/shopper_avatars/image/upload/w_200,h_200/sample_2.jpg', 'female', '1999-07-09', NOW());
-- 6. shopper_accounts (Depends on shoppers, organizations)
INSERT INTO shopper_accounts (id, primary_email, full_name, password, last_login_at, is_active, email_verified, tenant_id, referral_shareable_code, referrals_count, phone_number) VALUES
(500, 'shopper.jane@example.com', 'Jane Doe', 'hashed_pass_j1', NOW(), 1, 1, 100, 'JANE001AB', 1, NULL),
(501, 'shopper.john@example.com', 'John Smith', 'hashed_pass_j2', NULL, 1, 0, 100, 'JOHN999YZ', 0, NULL),
(502, 'shopper.amaka.uke@hotmail.com', 'Amaka Uke', 'hashed_pass_j3', NOW(), 1, 1, 100, 'AMAKA86UQ', 0, '+2348036689124');
-- 7. organization_teams (Depends on organizations)
INSERT INTO organization_teams (id, tenant_id, email, role) VALUES
(600, 100, 'manager.sam@fhn.com', 'manager'),
(601, 100, 'support.tina@fhn.com', 'support');
-- 8. organization_team_accounts (Depends on storefront_owner_teams, organizations)
INSERT INTO organization_team_accounts (id, email, password, profile_avatar_url, tenant_id) VALUES
(600, 'manager.sam@fhn.com', 'hashed_pass_m1', 'https://res.cloudinary.com/fhn-team_avatars/image/upload/w_220,h_220/headshot_1.jpg', 100),
(601, 'support.tina@fhn.com', 'hashed_pass_s1', 'https://res.cloudinary.com/fhn-team_avatars/image/upload/w_220,h_220/headshot_2.jpg', 100);
-- 9. shopper_shipping_addresses (Depends on shoppers, world_location_addresses)
INSERT INTO shopper_shipping_addresses (shopper_id, world_location_address_id) VALUES
(500, 400),
(501, 400);
-- 10. inventory_product_stock_manufacturers (Depends on organizations)
INSERT INTO inventory_product_stock_manufacturers (id, name, tenant_id, industry) VALUES
(700, 'NHN Couture', 100, 'fashion_wears'),
(701, 'House of Josh Designs', 100, 'fashion_wears');
-- 11. inventory_products (Depends on organizations)
INSERT INTO inventory_products (id, unit_name, unit_description, tenant_id, unit_image_url, refundable_on_return) VALUES
(800, 'Maxi Dress - Floral Print', 'A beautiful maxi dress with a vibrant floral pattern.', 100, 'https://res.cloudinary.com/product_unit_images/image/upload/maxi_floral_1.jpg', 1),
(801, 'Formal Suit - Men', 'Navy blue formal suit, slim fit.', 100, 'https://res.cloudinary.com/product_unit_images/image/upload/slimfit_suit_3.jpg', 1);
-- 12. inventory_categories (Depends on organizations, self-reference)
INSERT INTO inventory_categories (id, tenant_id, name, description, parent_category_id) VALUES
(900, 100, 'Dresses', 'One-piece & Multi-piece garments.', NULL),
(901, 100, 'Womenswear', 'Clothing for women.', 900),
(902, 100, 'Menswear', 'Clothing for men.', 900);
-- 13. inventory_catalog_product_categories (Depends on inventory_products, inventory_categories, organizations)
INSERT INTO inventory_catalog_product_categories (product_id, category_id, tenant_id) VALUES
(800, 900, 100),
(800, 901, 100),
(801, 902, 100);
-- 14. inventory_product_stock_variants (Depends on manufacturers, categories, organizations)
INSERT INTO inventory_product_stock_variants (id, sku_format, varaint_attributes, brand_id, tenant_id, variant_image_urls, category_id) VALUES
(1000, 'MD-RED-SM-COT-NHN', '{"sizes": ["sm", "lg"], "colors": ["red", "brown"], "styles": ["maxi", "slim-fit"], "brands": ["nhn_couture"], "materials": ["cotton"]}', 700, 100, '["https://res.cloudinary.com/product_variant_unit_images/image/upload/red_maxi_variant_1.jpg"]', 901),
(1001, 'FS-NVB-40-WOOL-JOSH', '{"sizes": ["40", "32"], "colors": ["navy_blue"], "styles": ["formal, "slim-fit"], "brands": ["house_of_josh"], "materials": ["wool"]}', 701, 100, '["https://res.cloudinary.com/product_variant_unit_images/image/upload/navy_blue_slimit_varaint_4.jpg"]', 902);
-- 15. inventory_product_stock_manifest (Depends on inventory_products, inventory_product_stock_variants, organizations)
INSERT INTO inventory_product_stock_manifest (id, stock_units_total_stored, stock_units_total_sold, unit_price_amount, unit_discount_price_amount, unit_price_currency, tenant_id, product_id, product_variant_id, stock_units_min_count_limit, stock_units_max_count_limit, unit_discount_price_amount_updated_at, stock_available_count_updated_at) VALUES
(1100, 50, 10, 1500000, 1200000, 'NGN-kobo', 100, 800, 1000, 5, 100, NOW(), NOW()), -- Maxi Dress (15000 NGN)
(1101, 20, 5, 4500000, 0, 'NGN-kobo', 100, 801, 1001, 2, 50, NOW(), NOW()); -- Formal Suit (45000 NGN)
-- 16. shopper_wishlists (Depends on shoppers, organizations)
INSERT INTO shopper_wishlists (id, shopper_id, tenant_id, wishlist_name, created_at, updated_at) VALUES
(1200, 500, 100, 'Summer Collection', NOW(), NOW());
-- 17. shoppers_wishlist_items (Depends on shopper_wishlists, inventory_products, organizations)
INSERT INTO shoppers_wishlist_items (id, wishlist_id, product_id, tenant_id, added_at) VALUES
(1300, 1200, 800, 100, NOW());
-- 18. shopping_carts (Depends on shoppers, organizations)
INSERT INTO shopping_carts (id, cart_reference, shopper_id, status, tenant_id, device_id, created_at, updated_at) VALUES
(1400, 'cuid-v2-ref-001', 500, 'in-use', 100, 'mobile-dev-id-001', NOW(), NOW()), -- Jane Doe's active cart
(1401, 'cuid-v2-ref-002', NULL, 'abandoned', 100, 'browser-fingerprint-999', DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY)); -- Guest cart, abandoned
-- 19. shopping_cart_items (Depends on shopping_carts, inventory_product_stock_manifest, organizations)
INSERT INTO shopping_cart_items (id, shopping_cart_id, product_stock_id, item_quantity, item_total, item_description, tenant_id, added_at) VALUES
(1500, 1400, 1100, 1, 0.00, 'Maxi Dress in Red, Size Small.', 100, NOW()); -- 1 of Maxi Dress (price in full currency for item_total for simplicity, though stock manifest uses sub-units)
-- 20. order_shipment_transit_delivery_methods (System table)
INSERT INTO order_shipment_transit_delivery_methods (id, transit_option, transit_type, transit_period) VALUES
(1600, 'bike', 'last-mile', 'same-day'),
(1601, 'van', 'last-mile', 'standard'),
(1602, 'truck', 'middle-mile', 'standard'),
(1603, 'truck', 'middle-mile', 'express-overnight'),
(1604, 'truck', 'first-mile', 'standard')
(1605, 'truck', 'white-glove', 'standard');
-- 21. order_shipment_transit_carriers (System table)
INSERT INTO order_shipment_transit_carriers (id, carrier_name, carrier_foul_up_rate) VALUES
(1700, 'KwikDelivery', '3%'),
(1701, 'FedEx', '1%'),
(1702, 'DHL', '2%');
-- 22. order_shipment_transit_carrier_stops (System table)
INSERT INTO order_shipment_transit_carrier_stops (id, carrier_id, type, stop_location_address) VALUES
(1800, 1702, 'consignor_drop-off', 'xxxxxxxxxx'), -- DHL picks-up consignments up from fulfilment center
(1801, 1700, 'transit_drop-off', 'oooooooooo'); -- KwikDelivery drops-off consignments to their collection center
-- 23. order_shipment_transit_carrier_delivery_methods (System table)
INSERT INTO order_shipment_transit_carrier_delivery_methods (carried_id, delivery_method_id) VALUES
(1700, 1601), -- KwikDelivery is a last mile delivery service
(1700, 1600), -- KwikDelivery is a last mile delivery service
(1702, 1602), -- DHL is a first-mile & middle-mile delivery service
(1702, 1603),
(1702, 1604);
-- 24. referral_rules (Depends on organizations)
INSERT INTO referral_rules (id, rule_valid_from_time, rule_valid_to_time, credit_for_referrer_user, credit_for_referred_user, credit_kickback_to_referrer, credit_kickback_referred_spend_minimum, tenant_id) VALUES
(1900, '2025-12-10 00:00:00', '2026-01-10 00:00:00', 500, 1000, 0.1, 50.00, 100);
-- 25. referral_manifest (Depends on shopper_accounts, referral_rules, organizations)
INSERT INTO referral_manifest (referrer_user_id, referred_user_id, used_at, tenant_id, rule_id) VALUES
(500, 502, NOW(), 100, 1900); -- Jane referred Amaka by giving Amaka her own referral code 'JANE001AB'
-- 26. orders (Depends on shoppers, organizations)
INSERT INTO orders (id, customer_id, tenant_id, checkout_status_started, checkout_status_completed, checkout_status_abandoned) VALUES
(2000, 500, 100, 0, 1, 0); -- Jane Doe's completed order
-- 27. order_invoices (Depends on shoppers, organizations)
INSERT INTO order_invoices (id, payer_id, tenant_id, total_fee_amount_paid, total_tax_amount_paid, due_date, note, order_items, payment_status_paid, payment_status_unpaid, payment_status_retried, payment_status_failed, created_at) VALUES
(2100, 500, 100, 30000.00, 2500.00, DATE_ADD(NOW(), INTERVAL 4 DAY), 'Order 2000 payment for Maxi Dress.', '{ "item_id_1100": { "quantity": 2, "item_total": 15000.00, "product_variant_id": 1000 } }', 1, 0, 0, 0, NOW());
-- 28. order_shipping_log (Depends on shoppers, organizations)
INSERT INTO order_shipping_log (id, total_fee_amount_paid, total_tax_amount_paid, tenant_id, consignee_id, tracking_number, shipment_status_shipped, shipment_status_in_transit, shipment_status_delivered, shipment_status_delayed) VALUES
(2200, 10.00, 1.00, 100, 500, 'TRKNG123456789', 1, 1, 0, 0);
-- 29. order_shipment_transit_manifest (Depends on shipping_log, carriers, carrier_stops, delivery_methods, organizations)
INSERT INTO order_shipment_transit_manifest (shipping_log_id, shipment_pickup_date, shipment_dropoff_date, shipment_carrier_ingress_stop_id, shipment_carrier_id, tenant_id, shipment_transit_delivery_method_id, shipment_carrier_stop_ordering, is_consignee_dropoff, requires_consignee_dropoff, shipment_carrier_stop_status_details) VALUES
(2200, DATE_ADD(NOW(), INTERVAL 1 DAY), DATE_ADD(NOW(), INTERVAL 1 DAY), 1800, 1702, 100, 1603, 'first', 0, 0, '{ "status": "Picked up from consignor and dropped off at first stop" }'),
(2201, DATE_ADD(NOW(), INTERVAL 3 DAY), DATE_ADD(NOW(), INTERVAL 4 DAY), 1801, 1700, 100, 1600, 'penultimate', 0, 1, '{ "status": "dropped off at collection center" }');
-- 30. order_basket_transactions (Depends on invoice, order, shopping_cart, shipping_log, shoppers, world_location_addresses, organizations)
INSERT INTO order_basket_transactions (invoice_id, order_id, pay_on_delivery, shopping_cart_id, payment_method, currency, shipment_start_date, shipment_end_date, shipment_carrier_stops_count, tenant_id, shipping_log_id, shopper_address_id) VALUES
(2100, 2000, 0, 1400, 'paystack', 'NGN', DATE_ADD(NOW(), INTERVAL 1 DAY), DATE_ADD(NOW(), INTERVAL 4 DAY), 2, 100, 2200, 400);
-- 31. installed_order_shipment_transit_carriers (Depends on organizations)
INSERT INTO installed_order_shipment_transit_carriers (id, tenant_id, carried_id) VALUES
(2400, 100, 1700),
(2401, 100, 1702);
-- 32. customer_emails ()
INSERT INTO customer_emails (id, email, is_primary) VALUES
(2500, 'shopper.jane@example.com', 1),
(2501, 'shopper.amaka.uke@hotmail.com', 1),
(2502, 'amaksbaby54@gmail.com', 0);
-- 33. shopper_emails (Depends on organizations)
INSERT INTO shopper_emails (email_id, tenant_id, shopper_id) VALUES
(2500, 100, 500),
(2501, 100, 502),
(2502, 100, 502);
INSERT INTO inventory_product_stock_variants (
id,
tenant_id,
brand_id,
category_id,
sku_format,
variant_attributes,
varaint_image_urls
) VALUES
(
4,
1,
2,
3,
'[Network]-[Color]-[Weight]-[OS]-[SIM Type]-[SIM Count]-[Display]-[Brand]-[Model]',
JSON_OBJECT(
"networks",
JSON_ARRAY("GSM", "CDMA", "HSPA", "EVDO"),
"bodies",
JSON_ARRAY("5.50_x_2.50_x_0.50_inches", "4.00_x_2.00_x_0.75_inches", "6.11_x_3.59_x_0.46_inches", "7.00_x_3.50_x_0.25_inches", "5.11 x 2.59 x 0.46 inches"),
"weights",
JSON_ARRAY("125_grams", "143_grams", "145_grams", "150_grams", "250_grams"),
"sim_types",
JSON_ARRAY("Nano-SIM" , "Micro-SIM"),
"sim_counts",
JSON_ARRAY("1", "2"),
"displays",
JSON_ARRAY("3.5_inches", "6.5_inches", "5.00_inches", "4.5_inches"),
"colors",
JSON_ARRAY("red", "black", "grey", "green", "white"),
"resolutions",
JSON_ARRAY("720_x_1280_pixels", "1920_x_1080_pixels"),
"oses" ,
JSON_ARRAY("Android_Lollipop_v4.4", "Android_Jellybean_v4.3", "Android_Marshmallow_v4.5", "Android_KitKat_v4.3")
),
JSON_ARRAY( -- use a URL shortner for these cloudinary secure URLs to conserve space in the JSON array here
"https://res.cloudinary.com/<tenant_name>/image/upload/sample_1.jpg|?color=red,green&os=Android_Jellybean_v4.3&weight=145_grams&resolutions=720_x_1280_pixels&sim_type=Micro-SIM&display=3.5_inches&sim_count=1&network=GSM,HSPA",
"https://res.cloudinary.com/<tenant_name>/image/upload/sample_2.jpg|?color=white,black,grey&os=Android_KitKat_v4.3&weight=150_grams&resolutions=720_x_1280_pixels&sim_type=Nano-SIM&display=3.5_inches&sim_count=2&network=GSM,CDMA,HSPA",
"https://res.cloudinary.com/<tenant_name>/image/upload/sample_3.jpg|?color=grey,green&os=Android_Marshmallow_v4.5&weight=250_grams&resolutions=720_x_1280_pixels&sim_type=Micro-SIM&display=5.00_inches&sim_count=1&network=GSM,CDMA"
)
);
@isocroft
Copy link
Author

isocroft commented Oct 26, 2025

-- [ INNER JOIN is used here because the tables involved in this query do not allow NULLs. If they did allow NULLs, LEFT JOIN will be used ]


-- Fetch all product categories that are not sub categories
SELECT
    name,
    description
FROM inventory_categories
WHERE parent_category_id IS NULL;


-- Fetch all product listings from inventory that are eligible to be displayed on the store-front based on a tenant (store-front owner) and a product category.
SELECT
     t1.unit_price_currency,
     t1.unit_price_amount,
     t1.product_variant_id,
     t2.unit_description,
     t2.unit_name,
     t2.unit_image_url,
     t2.refundable_on_return,
     t1.stock_units_total_stored - t1.stock_units_total_sold AS stock_units_total_available,
     CASE WHEN (t1.stock_units_total_stored - t1.stock_units_total_sold) > 0 AND TIMESTAMPDIFF(SECOND, t1.stock_available_count_updated_at, CURRENT_TIMESTAMP) <= 36800 THEN 'new' ELSE 'old' END AS stock_freshness_mark,
     CASE WHEN t1.unit_discount_price_amount > 0 AND TIMESTAMPDIFF(SECOND, t1.unit_discount_price_amount_updated_at, CURRENT_TIMESTAMP) <= 28200 THEN 1 ELSE 0 END AS has_new_discount_promotion
FROM inventory_products AS t2
INNER JOIN inventory_catalog_product_categories AS t3 ON t2.id = t3.product_id
INNER JOIN inventory_product_stock_manifest AS t1 ON t3.product_id = t1.product_id
WHERE t3.category_id = 902
AND t1.unit_price_amount > t1.unit_discount_price_amount
AND t1.stock_units_total_stored > t1.stock_units_total_sold
AND t2.tenant_id = 100
LIMIT 50;


-- Fetch all shopping cart and shopping cart items for a given shopper
SELECT
     t1.item_quantity,
     t1.item_total,
     t1.item_description,
     t1.added_at,
     t2.cart_reference,
     t2.status,
     t3.first_name,
     t3.last_name
FROM shoppers AS t3
LEFT JOIN shopping_carts AS t2 ON t3.id = t2.shopper_id
INNER JOIN shopping_cart_items AS t1 ON t2.id = t1.shopping_cart_id
WHERE t3.id = 500
GROUP BY t2.cart_reference
LIMIT 20;


-- Fetch business intelligence information for unpaid order payments that were never retried and are already overdue.
SELECT
    CASE WHEN orders.checkout_status_abandoned = 1 THEN 'abandoned' ELSE 'pending' END AS order_state,
    order_invoices.note,
    order_basket_transactions.pay_on_delivery
FROM order_basket_transactions
INNER JOIN order_invoices ON order_basket_transactions.invoice_id = order_invoices.id
INNER JOIN orders ON order_basket_transactions.order_id = orders.id
WHERE order_invoices.payment_status_unpaid = 1
AND order_invoices.payment_status_retried <> 1
AND orders.checkout_status_completed = 0
AND orders.checkout_status_started = 1
AND order_invoices.due_date > NOW()
GROUP BY order_basket_transactions.pay_on_delivery
LIMIT 20;

-- Fetch business intelligence information for successful order payments that were never retried and aren't yet overdue.
SELECT
     CASE WHEN orders.checkout_status_completed = 1 THEN 'completed' ELSE 'pending' END AS order_state,
     order_invoices.note,
     order_basket_transactions.payment_method,
     order_basket_transactions.pay_on_delivery
FROM order_basket_transactions
JOIN order_invoices ON order_basket_transactions.invoice_id = order_invoices.id
JOIN orders ON order_basket_transactions.order_id = orders.id
WHERE order_invoices.payment_status_paid = 1
AND order_invoices.payment_status_retried <> 1
AND orders.checkout_status_started = 1
AND orders.checkout_status_abandoned <> 1
AND order_invoices.due_date < NOW()
GROUP BY order_basket_transactions.payment_method
LIMIT 20;

-- Fetch all shopping carts + cart items along with order invoice and order shipping information for a given tenant
SELECT
    shopping_carts.cart_reference,
    order_invoices.total_fee_amount_paid,
    order_invoices.total_tax_amount_paid,
    order_invoices.due_date,
    order_invoices.note,
    world_location_addresses.street_name,
    world_location_addresses.house_number,
    world_location_addresses.zipcode,
    world_location_addresses.city,
    order_shipping_log.tracking_number,
    order_shipping_log.total_fee_amount_paid,
    order_shipping_log.total_tax_amount_paid,
    shopping_cart_items.product_stock_id
FROM order_basket_transactions
INNER JOIN orders ON order_basket_transactions.order_id = orders.id
INNER JOIN order_invoices ON order_basket_transactions.invoice_id = order_invoices.id
INNER JOIN order_shipping_log ON order_basket_transactions.shipping_log_id = order_shipping_log.id
INNER JOIN shopping_carts ON order_basket_transactions.shopping_cart_id = shopping_carts.id
INNER JOIN world_location_addresses ON order_basket_transactions.shopper_address_id = world_location_addresses.id
INNER JOIN shopping_cart_items ON shopping_carts.id = shopping_cart_items.shopping_cart_id
WHERE
    order_basket_transactions.tenant_id = 100 AND
    orders.checkout_status_completed <> 1 AND  -- Order MUST be incomplete
    shopping_carts.status NOT IN ('picked', 'returned') AND -- Cart status NOT 'picked' or 'returned'
    order_invoices.payment_status_overdue <> 1 AND -- Invoice NOT overdue
    order_invoices.payment_status_failed <> 1 AND  -- Invoice NOT failed
    order_shipping_log.shipment_status_shipped <> 1 AND -- Shipment NOT shipped
    order_shipping_log.shipment_status_in_transit <> 1 AND -- Shipment NOT in transit
    order_shipping_log.shipment_status_delivered <> 1 AND -- Shipment NOT delivered
    order_shipping_log.shipment_status_delayed <> 1 -- Shipment NOT delayed
GROUP BY shopping_carts.cart_reference
LIMIT 50;


--- shoppers.id === orders.customer_id
--- shoppers.id === order_invoices.payer_id
--- shoppers.id === shopper_shipping_addresses.shopper_id
--- shoppers.id = order_shipping_log.consignee_id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment