1use crate::CommerceModelError;
2use coil_auth::Capability;
3use coil_core::{AdminContributionKind, AdminNavigationSection, AdminResourceContribution};
4use coil_data::{MigrationId, MigrationOwner, MigrationPlan, MigrationStep};
5
6pub struct CommerceModule {
7 name: String,
8 config_namespace: String,
9 admin_resources: Vec<AdminResourceContribution>,
10}
11
12impl CommerceModule {
13 pub fn new() -> Self {
14 Self {
15 name: "commerce".to_string(),
16 config_namespace: "commerce".to_string(),
17 admin_resources: vec![
18 AdminResourceContribution::new(
19 "commerce.catalog",
20 "/admin/commerce/catalog",
21 "Catalog",
22 "Catalog",
23 AdminNavigationSection::Commerce,
24 AdminContributionKind::ResourceIndex,
25 Capability::CatalogProductRead,
26 ),
27 AdminResourceContribution::new(
28 "commerce.collections",
29 "/admin/commerce/collections",
30 "Collections",
31 "Collections",
32 AdminNavigationSection::Commerce,
33 AdminContributionKind::ResourceIndex,
34 Capability::CatalogCollectionEdit,
35 ),
36 AdminResourceContribution::new(
37 "commerce.checkouts",
38 "/admin/commerce/checkouts",
39 "Checkouts",
40 "Checkouts",
41 AdminNavigationSection::Commerce,
42 AdminContributionKind::ResourceIndex,
43 Capability::CheckoutSessionCreate,
44 ),
45 AdminResourceContribution::new(
46 "commerce.orders",
47 "/admin/commerce/orders",
48 "Orders",
49 "Orders",
50 AdminNavigationSection::Commerce,
51 AdminContributionKind::ResourceIndex,
52 Capability::OrderRead,
53 ),
54 ],
55 }
56 }
57
58 pub fn name(&self) -> &str {
59 &self.name
60 }
61
62 pub fn config_namespace(&self) -> &str {
63 &self.config_namespace
64 }
65
66 pub fn admin_resources(&self) -> &[AdminResourceContribution] {
67 &self.admin_resources
68 }
69
70 pub fn migration_plan(&self) -> Result<MigrationPlan, CommerceModelError> {
71 let owner = MigrationOwner::Module(self.name.clone());
72 let mut plan = MigrationPlan::new();
73 plan.insert(
74 MigrationStep::new(
75 MigrationId::new("001_catalog_products")?,
76 owner.clone(),
77 10,
78 "create catalog products and variants tables",
79 )?
80 .with_statement(
81 "CREATE TABLE IF NOT EXISTS commerce_catalog_products (id TEXT PRIMARY KEY, slug TEXT NOT NULL, sku TEXT NOT NULL, title TEXT NOT NULL, product_type TEXT NOT NULL, status TEXT NOT NULL, price_minor BIGINT NOT NULL, currency TEXT NOT NULL, source_system TEXT, source_key TEXT UNIQUE, import_batch_id TEXT, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
82 )?
83 .with_statement(
84 "CREATE TABLE IF NOT EXISTS commerce_catalog_variants (id TEXT PRIMARY KEY, product_id TEXT NOT NULL, sku TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL, price_minor BIGINT NOT NULL, currency TEXT NOT NULL, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
85 )?,
86 )?;
87 plan.insert(
88 MigrationStep::new(
89 MigrationId::new("002_collections")?,
90 owner.clone(),
91 20,
92 "create collections and product membership tables",
93 )?
94 .with_statement(
95 "CREATE TABLE IF NOT EXISTS commerce_collections (id TEXT PRIMARY KEY, handle TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
96 )?
97 .with_statement(
98 "CREATE TABLE IF NOT EXISTS commerce_collection_products (collection_id TEXT NOT NULL, product_id TEXT NOT NULL, position BIGINT NOT NULL, PRIMARY KEY (collection_id, product_id))",
99 )?,
100 )?;
101 plan.insert(
102 MigrationStep::new(
103 MigrationId::new("003_checkouts_orders")?,
104 owner.clone(),
105 30,
106 "create checkout, order, and pricing snapshot tables",
107 )?
108 .with_statement(
109 "CREATE TABLE IF NOT EXISTS commerce_checkouts (id TEXT PRIMARY KEY, status TEXT NOT NULL, currency TEXT NOT NULL, email TEXT, principal_id TEXT, subtotal_minor BIGINT NOT NULL, total_minor BIGINT NOT NULL, payment_reference TEXT, source_system TEXT, source_key TEXT UNIQUE, import_batch_id TEXT, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
110 )?
111 .with_statement(
112 "CREATE TABLE IF NOT EXISTS commerce_orders (id TEXT PRIMARY KEY, checkout_id TEXT, status TEXT NOT NULL, currency TEXT NOT NULL, email TEXT, principal_id TEXT, subtotal_minor BIGINT NOT NULL, total_minor BIGINT NOT NULL, payment_status TEXT NOT NULL, payment_reference TEXT, source_system TEXT, source_key TEXT UNIQUE, import_batch_id TEXT, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
113 )?
114 .with_statement(
115 "CREATE TABLE IF NOT EXISTS commerce_order_lines (id TEXT PRIMARY KEY, order_id TEXT NOT NULL, product_id TEXT, variant_id TEXT, title TEXT NOT NULL, quantity BIGINT NOT NULL, line_total_minor BIGINT NOT NULL, currency TEXT NOT NULL)",
116 )?,
117 )?;
118 plan.insert(
119 MigrationStep::new(
120 MigrationId::new("004_refunds")?,
121 owner,
122 40,
123 "create refund ledger and payment reconciliation tables",
124 )?
125 .with_statement(
126 "CREATE TABLE IF NOT EXISTS commerce_refunds (id TEXT PRIMARY KEY, order_id TEXT NOT NULL, status TEXT NOT NULL, amount_minor BIGINT NOT NULL, currency TEXT NOT NULL, reason TEXT NOT NULL, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
127 )?
128 .with_statement(
129 "CREATE TABLE IF NOT EXISTS commerce_payment_reconciliation (id TEXT PRIMARY KEY, order_id TEXT NOT NULL, provider TEXT NOT NULL, provider_reference TEXT NOT NULL, status TEXT NOT NULL, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
130 )?,
131 )?;
132 Ok(plan)
133 }
134}
135
136impl Default for CommerceModule {
137 fn default() -> Self {
138 Self::new()
139 }
140}