@base44/app-plugin-commerce 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
- package/base44/entities/commerce.Cart.jsonc +73 -0
- package/base44/entities/commerce.Coupon.jsonc +113 -0
- package/base44/entities/commerce.Customer.jsonc +96 -0
- package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
- package/base44/entities/commerce.EmailLog.jsonc +43 -0
- package/base44/entities/commerce.Order.jsonc +287 -0
- package/base44/entities/commerce.OrderNote.jsonc +31 -0
- package/base44/entities/commerce.OrderRefund.jsonc +64 -0
- package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
- package/base44/entities/commerce.Product.jsonc +291 -0
- package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
- package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
- package/base44/entities/commerce.ProductCategory.jsonc +51 -0
- package/base44/entities/commerce.ProductReview.jsonc +48 -0
- package/base44/entities/commerce.ProductTag.jsonc +30 -0
- package/base44/entities/commerce.ProductVariation.jsonc +167 -0
- package/base44/entities/commerce.ShippingClass.jsonc +30 -0
- package/base44/entities/commerce.ShippingZone.jsonc +41 -0
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
- package/base44/entities/commerce.StoreSettings.jsonc +23 -0
- package/base44/entities/commerce.TaxClass.jsonc +23 -0
- package/base44/entities/commerce.TaxRate.jsonc +68 -0
- package/base44/entities/commerce.Webhook.jsonc +57 -0
- package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
- package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
- package/base44/functions/commerce/admin-customers/entry.ts +141 -0
- package/base44/functions/commerce/admin-orders/entry.ts +396 -0
- package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
- package/base44/functions/commerce/admin-products/entry.ts +506 -0
- package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
- package/base44/functions/commerce/admin-reports/entry.ts +283 -0
- package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
- package/base44/functions/commerce/admin-tools/entry.ts +261 -0
- package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
- package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
- package/base44/functions/commerce/payments/entry.ts +238 -0
- package/base44/functions/commerce/seed-store/defaults.ts +162 -0
- package/base44/functions/commerce/seed-store/entry.ts +310 -0
- package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
- package/base44/functions/commerce/storefront-account/entry.ts +207 -0
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
- package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
- package/base44/shared/commerce/auth.ts +60 -0
- package/base44/shared/commerce/coupons.ts +257 -0
- package/base44/shared/commerce/data/continents.ts +75 -0
- package/base44/shared/commerce/data/countries.ts +307 -0
- package/base44/shared/commerce/data/currencies.ts +46 -0
- package/base44/shared/commerce/email-templates.ts +240 -0
- package/base44/shared/commerce/emails.ts +225 -0
- package/base44/shared/commerce/money.ts +66 -0
- package/base44/shared/commerce/orders.ts +251 -0
- package/base44/shared/commerce/payments.ts +495 -0
- package/base44/shared/commerce/reviews.ts +36 -0
- package/base44/shared/commerce/scan.ts +57 -0
- package/base44/shared/commerce/sequence.ts +35 -0
- package/base44/shared/commerce/settings.ts +57 -0
- package/base44/shared/commerce/shipping.ts +215 -0
- package/base44/shared/commerce/stock.ts +227 -0
- package/base44/shared/commerce/stripe.ts +463 -0
- package/base44/shared/commerce/tax.ts +136 -0
- package/base44/shared/commerce/totals.ts +314 -0
- package/base44/shared/commerce/webhooks.ts +116 -0
- package/package.json +37 -0
- package/scripts/install.js +156 -0
- package/skills/commerce/SKILL.md +62 -0
- package/skills/commerce/docs/api-admin.md +186 -0
- package/skills/commerce/docs/api-storefront.md +408 -0
- package/skills/commerce/installation-guidelines.md +91 -0
- package/skills/commerce/post-installation.md +157 -0
- package/skills/commerce/references/emails.md +13 -0
- package/skills/commerce/references/guest-access-security.md +18 -0
- package/skills/commerce/references/limits-and-performance.md +16 -0
- package/skills/commerce/references/media-and-downloads.md +4 -0
- package/skills/commerce/references/online-payments.md +201 -0
- package/skills/commerce/references/product-render.md +87 -0
- package/skills/commerce/references/scheduled-work.md +19 -0
- package/skills/commerce/references/storefront-product-page.md +83 -0
- package/skills/commerce/references/webhooks.md +8 -0
- package/src/commerce/admin/README.md +107 -0
- package/src/commerce/admin/bot/Markdown.jsx +138 -0
- package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
- package/src/commerce/admin/bot/pipe-tables.js +116 -0
- package/src/commerce/admin/components/AddressForm.jsx +78 -0
- package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
- package/src/commerce/admin/components/CountrySelect.jsx +81 -0
- package/src/commerce/admin/components/DataTable.jsx +192 -0
- package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
- package/src/commerce/admin/components/EmptyState.jsx +17 -0
- package/src/commerce/admin/components/MediaUploader.jsx +116 -0
- package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
- package/src/commerce/admin/components/MoneyInput.jsx +50 -0
- package/src/commerce/admin/components/PageHeader.jsx +29 -0
- package/src/commerce/admin/components/RichTextarea.jsx +21 -0
- package/src/commerce/admin/components/SearchSelect.jsx +142 -0
- package/src/commerce/admin/components/StatusBadge.jsx +17 -0
- package/src/commerce/admin/context/BasePathContext.jsx +26 -0
- package/src/commerce/admin/context/SettingsContext.jsx +207 -0
- package/src/commerce/admin/hooks/useAsync.js +46 -0
- package/src/commerce/admin/hooks/useDebounce.js +11 -0
- package/src/commerce/admin/hooks/useMoney.js +52 -0
- package/src/commerce/admin/hooks/usePagedList.js +83 -0
- package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
- package/src/commerce/admin/hooks/useRealtime.js +129 -0
- package/src/commerce/admin/index.jsx +34 -0
- package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
- package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
- package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
- package/src/commerce/admin/layout/Sidebar.jsx +130 -0
- package/src/commerce/admin/layout/Topbar.jsx +94 -0
- package/src/commerce/admin/lib/api.js +55 -0
- package/src/commerce/admin/lib/constants.js +157 -0
- package/src/commerce/admin/lib/format.js +27 -0
- package/src/commerce/admin/lib/geo-data.js +125 -0
- package/src/commerce/admin/lib/order-utils.js +147 -0
- package/src/commerce/admin/lib/paths.js +35 -0
- package/src/commerce/admin/lib/product-utils.js +55 -0
- package/src/commerce/admin/pages/Dashboard.jsx +245 -0
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
- package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
- package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
- package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
- package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
- package/src/commerce/admin/pages/products/Categories.jsx +236 -0
- package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
- package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
- package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
- package/src/commerce/admin/pages/products/Tags.jsx +150 -0
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
- package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
- package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
- package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
- package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
- package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
- package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
- package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
- package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
- package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
- package/src/commerce/admin/routes.jsx +151 -0
- package/src/commerce/utils/index.js +19 -0
- package/src/commerce/utils/shipping-promos.js +99 -0
- package/src/commerce/utils/variants.js +411 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.ShippingClass",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "Shipping class assignable to products; flat-rate methods can charge per class.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1
|
|
9
|
+
},
|
|
10
|
+
"slug": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Unique slug (enforced in admin UI / functions)"
|
|
13
|
+
},
|
|
14
|
+
"description": {
|
|
15
|
+
"type": "string"
|
|
16
|
+
},
|
|
17
|
+
"count": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"default": 0,
|
|
20
|
+
"description": "Derived product count. Maintained by commerce/admin-products; repair via commerce/admin-tools recount-terms."
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"required": ["name"],
|
|
24
|
+
"rls": {
|
|
25
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
26
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
27
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
28
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.ShippingZone",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "Geographic shipping zone. Zones are matched by 'order' ascending; the seeded 'Rest of the world' zone (order 999, no locations) catches unmatched addresses.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1
|
|
9
|
+
},
|
|
10
|
+
"order": {
|
|
11
|
+
"type": "integer",
|
|
12
|
+
"default": 0,
|
|
13
|
+
"description": "Match priority, ascending"
|
|
14
|
+
},
|
|
15
|
+
"locations": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"description": "Zone locations; empty = matches everything (fallback zone)",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"type": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": ["country", "state", "postcode", "continent"],
|
|
24
|
+
"description": "Location type"
|
|
25
|
+
},
|
|
26
|
+
"code": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Country code (US), state code (US:CA), postcode pattern (90*, 1000...2000), or continent code (EU)"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": ["name"],
|
|
35
|
+
"rls": {
|
|
36
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
37
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
38
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
39
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.ShippingZoneMethod",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "A shipping method instance attached to a zone. The settings object shape depends on method_id.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"zone_id": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"description": "ShippingZone id"
|
|
9
|
+
},
|
|
10
|
+
"method_id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": ["flat_rate", "free_shipping", "local_pickup"],
|
|
13
|
+
"description": "Method type"
|
|
14
|
+
},
|
|
15
|
+
"title": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Customer-facing method title"
|
|
18
|
+
},
|
|
19
|
+
"enabled": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"default": true
|
|
22
|
+
},
|
|
23
|
+
"order": {
|
|
24
|
+
"type": "integer",
|
|
25
|
+
"default": 0,
|
|
26
|
+
"description": "Display order within the zone"
|
|
27
|
+
},
|
|
28
|
+
"settings": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"description": "Per-method settings. flat_rate: {cost, tax_status, class_costs, no_class_cost, calculation_type}; free_shipping: {requires, min_amount, ignore_discounts}; local_pickup: {cost, tax_status}",
|
|
31
|
+
"properties": {
|
|
32
|
+
"cost": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "flat_rate/local_pickup base cost"
|
|
35
|
+
},
|
|
36
|
+
"tax_status": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["taxable", "none"],
|
|
39
|
+
"description": "flat_rate/local_pickup"
|
|
40
|
+
},
|
|
41
|
+
"class_costs": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"description": "flat_rate: additional cost per shipping class present in the cart",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"shipping_class_id": { "type": "string" },
|
|
48
|
+
"cost": { "type": "number" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"no_class_cost": {
|
|
53
|
+
"type": "number",
|
|
54
|
+
"description": "flat_rate: cost applied for items without a shipping class"
|
|
55
|
+
},
|
|
56
|
+
"calculation_type": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["class", "order"],
|
|
59
|
+
"description": "flat_rate: charge per class present (class) or most expensive class only (order)"
|
|
60
|
+
},
|
|
61
|
+
"requires": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"enum": ["", "coupon", "min_amount", "either", "both"],
|
|
64
|
+
"description": "free_shipping: requirement for the method to be offered"
|
|
65
|
+
},
|
|
66
|
+
"min_amount": {
|
|
67
|
+
"type": "number",
|
|
68
|
+
"description": "free_shipping: minimum items subtotal"
|
|
69
|
+
},
|
|
70
|
+
"ignore_discounts": {
|
|
71
|
+
"type": "boolean",
|
|
72
|
+
"description": "free_shipping: compare min_amount against pre-discount subtotal"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"required": ["zone_id", "method_id"],
|
|
78
|
+
"rls": {
|
|
79
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
80
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
81
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
82
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.StoreSettings",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "Store configuration, one record per settings group. The values object shape is group-specific; seeded by commerce/seed-store. Storefront functions expose only a safe subset via commerce/storefront-catalog get-store-info.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"group_id": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"enum": ["general", "products", "inventory", "downloadable", "tax", "shipping", "emails"],
|
|
9
|
+
"description": "Settings group; unique (one record per group, enforced by seed/admin UI)"
|
|
10
|
+
},
|
|
11
|
+
"values": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"description": "Free-form settings for the group"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"required": ["group_id"],
|
|
17
|
+
"rls": {
|
|
18
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
19
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
20
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
21
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.TaxClass",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "Tax class grouping products for rate matching. Seeded: standard, reduced-rate, zero-rate.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"slug": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1,
|
|
9
|
+
"description": "Unique slug (enforced in admin UI / seed)"
|
|
10
|
+
},
|
|
11
|
+
"name": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"minLength": 1
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"required": ["slug", "name"],
|
|
17
|
+
"rls": {
|
|
18
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
19
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
20
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
21
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.TaxRate",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "A tax rate matched by location and tax class. Applied by shared/tax per priority/compound rules.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"country": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"default": "",
|
|
9
|
+
"description": "ISO 3166-1 alpha-2 country code; empty = any"
|
|
10
|
+
},
|
|
11
|
+
"state": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "",
|
|
14
|
+
"description": "State/province code; empty = any"
|
|
15
|
+
},
|
|
16
|
+
"postcodes": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": { "type": "string" },
|
|
19
|
+
"description": "Postcode patterns; supports wildcard (90*) and numeric range (1000...2000); empty = any"
|
|
20
|
+
},
|
|
21
|
+
"cities": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": { "type": "string" },
|
|
24
|
+
"description": "City names; empty = any"
|
|
25
|
+
},
|
|
26
|
+
"rate": {
|
|
27
|
+
"type": "number",
|
|
28
|
+
"description": "Percentage rate, e.g. 20 for 20%"
|
|
29
|
+
},
|
|
30
|
+
"name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"minLength": 1,
|
|
33
|
+
"description": "Label, e.g. VAT"
|
|
34
|
+
},
|
|
35
|
+
"priority": {
|
|
36
|
+
"type": "integer",
|
|
37
|
+
"default": 1,
|
|
38
|
+
"description": "Only one matching rate per priority is applied; lower priorities apply first"
|
|
39
|
+
},
|
|
40
|
+
"compound": {
|
|
41
|
+
"type": "boolean",
|
|
42
|
+
"default": false,
|
|
43
|
+
"description": "Applied on top of previously accumulated taxes"
|
|
44
|
+
},
|
|
45
|
+
"shipping": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"default": true,
|
|
48
|
+
"description": "Rate also applies to shipping"
|
|
49
|
+
},
|
|
50
|
+
"tax_class": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"default": "standard",
|
|
53
|
+
"description": "TaxClass slug this rate belongs to"
|
|
54
|
+
},
|
|
55
|
+
"menu_order": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"default": 0,
|
|
58
|
+
"description": "Tie-breaker among rates of the same priority"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": ["rate", "name"],
|
|
62
|
+
"rls": {
|
|
63
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
64
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
65
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
66
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.Webhook",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "Outgoing webhook definition. Dispatched by shared/commerce/webhooks with X-Commerce-Webhook-* headers and HMAC-SHA256 signature.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1
|
|
9
|
+
},
|
|
10
|
+
"status": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": ["active", "paused", "disabled"],
|
|
13
|
+
"default": "active",
|
|
14
|
+
"description": "Only active webhooks are delivered; auto-disabled after 5 consecutive failures"
|
|
15
|
+
},
|
|
16
|
+
"topic": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "resource.event, e.g. order.created, product.updated"
|
|
19
|
+
},
|
|
20
|
+
"resource": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": ["order", "product", "customer", "coupon"],
|
|
23
|
+
"description": "Derived from topic"
|
|
24
|
+
},
|
|
25
|
+
"event": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["created", "updated", "deleted", "restored"],
|
|
28
|
+
"description": "Derived from topic"
|
|
29
|
+
},
|
|
30
|
+
"delivery_url": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"format": "uri",
|
|
33
|
+
"description": "Endpoint receiving POST deliveries"
|
|
34
|
+
},
|
|
35
|
+
"secret": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "HMAC-SHA256 signing secret (see skills/commerce/references/webhooks.md on storage tradeoffs)"
|
|
38
|
+
},
|
|
39
|
+
"api_version": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"default": "v3",
|
|
42
|
+
"description": "Payload format version"
|
|
43
|
+
},
|
|
44
|
+
"failure_count": {
|
|
45
|
+
"type": "integer",
|
|
46
|
+
"default": 0,
|
|
47
|
+
"description": "Consecutive delivery failures. Maintained by shared/commerce/webhooks; auto-disable at 5."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": ["name", "topic", "delivery_url"],
|
|
51
|
+
"rls": {
|
|
52
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
53
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
54
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
55
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commerce.WebhookDelivery",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"description": "Delivery log for a webhook attempt. Written by shared/commerce/webhooks dispatch; prune via commerce/admin-tools prune-webhook-deliveries.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"webhook_id": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"description": "Webhook id"
|
|
9
|
+
},
|
|
10
|
+
"topic": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"delivery_url": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"request_headers": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"description": "Headers sent with the delivery"
|
|
19
|
+
},
|
|
20
|
+
"request_body": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "JSON payload, truncated to 32KB"
|
|
23
|
+
},
|
|
24
|
+
"response_code": {
|
|
25
|
+
"type": "integer"
|
|
26
|
+
},
|
|
27
|
+
"response_body": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Response body, truncated"
|
|
30
|
+
},
|
|
31
|
+
"duration_ms": {
|
|
32
|
+
"type": "number"
|
|
33
|
+
},
|
|
34
|
+
"success": {
|
|
35
|
+
"type": "boolean"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": ["webhook_id"],
|
|
39
|
+
"rls": {
|
|
40
|
+
"read": { "user_condition": { "role": "admin" } },
|
|
41
|
+
"create": { "user_condition": { "role": "admin" } },
|
|
42
|
+
"update": { "user_condition": { "role": "admin" } },
|
|
43
|
+
"delete": { "user_condition": { "role": "admin" } }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commerce/admin-coupons — coupon CRUD with code normalization/uniqueness and webhooks.
|
|
3
|
+
*
|
|
4
|
+
* Actions: save | delete | batch | search
|
|
5
|
+
*/
|
|
6
|
+
import { createClientFromRequest } from "npm:@base44/sdk";
|
|
7
|
+
import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
|
|
8
|
+
import { dispatch } from "../../../shared/commerce/webhooks.ts";
|
|
9
|
+
import { pageSlice, scanAll, textMatch } from "../../../shared/commerce/scan.ts";
|
|
10
|
+
|
|
11
|
+
const ok = (data: unknown, status = 200) => Response.json({ success: true, data }, { status });
|
|
12
|
+
const fail = (status: number, error: string, code?: string) =>
|
|
13
|
+
Response.json({ success: false, error, code }, { status });
|
|
14
|
+
|
|
15
|
+
Deno.serve(async (req) => {
|
|
16
|
+
try {
|
|
17
|
+
const base44 = createClientFromRequest(req);
|
|
18
|
+
await requireAdmin(base44);
|
|
19
|
+
const sr = base44.asServiceRole;
|
|
20
|
+
const { action, ...payload } = await req.json();
|
|
21
|
+
|
|
22
|
+
switch (action) {
|
|
23
|
+
case "save":
|
|
24
|
+
return ok(await save(sr, payload.coupon));
|
|
25
|
+
case "delete":
|
|
26
|
+
return ok(await remove(sr, payload.id));
|
|
27
|
+
case "batch":
|
|
28
|
+
return ok(await batch(sr, payload));
|
|
29
|
+
case "search": {
|
|
30
|
+
const { q, limit = 20, skip = 0, sort = "-created_date" } = payload;
|
|
31
|
+
let rows = await scanAll(sr.entities["commerce.Coupon"], null, sort);
|
|
32
|
+
if (q) rows = rows.filter((c) => textMatch(c.code, q) || textMatch(c.description, q));
|
|
33
|
+
return ok(pageSlice(rows, Number(limit), Number(skip)));
|
|
34
|
+
}
|
|
35
|
+
default:
|
|
36
|
+
return fail(400, `Unknown action: ${action}`, "unknown_action");
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e instanceof HttpError) return fail(e.status, e.message, e.code);
|
|
40
|
+
console.error("commerce/admin-coupons error:", e);
|
|
41
|
+
return fail(500, (e as Error).message ?? "Internal error");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function save(sr: any, coupon: any): Promise<any> {
|
|
46
|
+
if (!coupon) throw new HttpError(400, "coupon is required", "invalid_payload");
|
|
47
|
+
const code = String(coupon.code ?? "").toLowerCase().trim();
|
|
48
|
+
if (!code) throw new HttpError(400, "Coupon code is required.", "invalid_payload");
|
|
49
|
+
if (coupon.amount == null || Number(coupon.amount) < 0) {
|
|
50
|
+
throw new HttpError(400, "Coupon amount is required.", "invalid_amount");
|
|
51
|
+
}
|
|
52
|
+
if (coupon.discount_type === "percent" && Number(coupon.amount) > 100) {
|
|
53
|
+
throw new HttpError(400, "Percentage discounts cannot exceed 100.", "invalid_amount");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const dupes = (await sr.entities["commerce.Coupon"].filter({ code }, undefined, 2)) ?? [];
|
|
57
|
+
if (dupes.some((c: any) => c.id !== coupon.id)) {
|
|
58
|
+
throw new HttpError(409, `Coupon code "${code}" already exists.`, "duplicate_code");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { id, created_date: _cd, updated_date: _ud, created_by: _cb, ...fields } = { ...coupon, code };
|
|
62
|
+
let saved: any;
|
|
63
|
+
if (id) {
|
|
64
|
+
const prev = await sr.entities["commerce.Coupon"].get(id);
|
|
65
|
+
if (!prev) throw new HttpError(404, "Coupon not found", "not_found");
|
|
66
|
+
await sr.entities["commerce.Coupon"].update(id, fields);
|
|
67
|
+
saved = { ...prev, ...fields };
|
|
68
|
+
await dispatch(sr, "coupon.updated", saved);
|
|
69
|
+
} else {
|
|
70
|
+
saved = await sr.entities["commerce.Coupon"].create({ usage_count: 0, used_by: [], ...fields });
|
|
71
|
+
await dispatch(sr, "coupon.created", saved);
|
|
72
|
+
}
|
|
73
|
+
return saved;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function remove(sr: any, id: string): Promise<any> {
|
|
77
|
+
const coupon = await sr.entities["commerce.Coupon"].get(id);
|
|
78
|
+
if (!coupon) throw new HttpError(404, "Coupon not found", "not_found");
|
|
79
|
+
await sr.entities["commerce.Coupon"].delete(id);
|
|
80
|
+
await dispatch(sr, "coupon.deleted", coupon);
|
|
81
|
+
return { deleted: id };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function batch(sr: any, payload: any): Promise<any> {
|
|
85
|
+
const results = { create: [] as any[], update: [] as any[], delete: [] as any[] };
|
|
86
|
+
const cap = 100;
|
|
87
|
+
for (const c of (payload.create ?? []).slice(0, cap)) {
|
|
88
|
+
try { results.create.push({ success: true, coupon: await save(sr, c) }); }
|
|
89
|
+
catch (e) { results.create.push({ success: false, error: (e as Error).message }); }
|
|
90
|
+
}
|
|
91
|
+
for (const c of (payload.update ?? []).slice(0, cap)) {
|
|
92
|
+
try { results.update.push({ success: true, coupon: await save(sr, c) }); }
|
|
93
|
+
catch (e) { results.update.push({ success: false, id: c.id, error: (e as Error).message }); }
|
|
94
|
+
}
|
|
95
|
+
for (const id of (payload.delete ?? []).slice(0, cap)) {
|
|
96
|
+
try { results.delete.push({ success: true, ...(await remove(sr, id)) }); }
|
|
97
|
+
catch (e) { results.delete.push({ success: false, id, error: (e as Error).message }); }
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commerce/admin-customers — customer CRUD, invites and denormalized stats.
|
|
3
|
+
*
|
|
4
|
+
* Actions: save | delete | search | invite | recalculate-stats
|
|
5
|
+
*/
|
|
6
|
+
import { createClientFromRequest } from "npm:@base44/sdk";
|
|
7
|
+
import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
|
|
8
|
+
import { round2 } from "../../../shared/commerce/money.ts";
|
|
9
|
+
import { dispatch } from "../../../shared/commerce/webhooks.ts";
|
|
10
|
+
import { pageSlice, scanAll, textMatch } from "../../../shared/commerce/scan.ts";
|
|
11
|
+
|
|
12
|
+
const ok = (data: unknown, status = 200) => Response.json({ success: true, data }, { status });
|
|
13
|
+
const fail = (status: number, error: string, code?: string) =>
|
|
14
|
+
Response.json({ success: false, error, code }, { status });
|
|
15
|
+
|
|
16
|
+
Deno.serve(async (req) => {
|
|
17
|
+
try {
|
|
18
|
+
const base44 = createClientFromRequest(req);
|
|
19
|
+
await requireAdmin(base44);
|
|
20
|
+
const sr = base44.asServiceRole;
|
|
21
|
+
const { action, ...payload } = await req.json();
|
|
22
|
+
|
|
23
|
+
switch (action) {
|
|
24
|
+
case "save":
|
|
25
|
+
return ok(await save(sr, payload.customer));
|
|
26
|
+
case "delete":
|
|
27
|
+
return ok(await remove(sr, payload));
|
|
28
|
+
case "search": {
|
|
29
|
+
const { q, limit = 20, skip = 0, sort = "-created_date" } = payload;
|
|
30
|
+
let rows = await scanAll(sr.entities["commerce.Customer"], null, sort);
|
|
31
|
+
if (q) {
|
|
32
|
+
rows = rows.filter((c) =>
|
|
33
|
+
textMatch(c.email, q) || textMatch(c.username, q) ||
|
|
34
|
+
textMatch(`${c.first_name ?? ""} ${c.last_name ?? ""}`, q)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return ok(pageSlice(rows, Number(limit), Number(skip)));
|
|
38
|
+
}
|
|
39
|
+
case "invite":
|
|
40
|
+
return ok(await invite(base44, sr, payload));
|
|
41
|
+
case "recalculate-stats": {
|
|
42
|
+
const customer = await getCustomer(sr, payload.customer_id);
|
|
43
|
+
return ok(await recalcStats(sr, customer));
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
return fail(400, `Unknown action: ${action}`, "unknown_action");
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (e instanceof HttpError) return fail(e.status, e.message, e.code);
|
|
50
|
+
console.error("commerce/admin-customers error:", e);
|
|
51
|
+
return fail(500, (e as Error).message ?? "Internal error");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
async function getCustomer(sr: any, id: string): Promise<any> {
|
|
56
|
+
if (!id) throw new HttpError(400, "customer_id is required", "invalid_payload");
|
|
57
|
+
const customer = await sr.entities["commerce.Customer"].get(id);
|
|
58
|
+
if (!customer) throw new HttpError(404, "Customer not found", "not_found");
|
|
59
|
+
return customer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function save(sr: any, customer: any): Promise<any> {
|
|
63
|
+
if (!customer) throw new HttpError(400, "customer is required", "invalid_payload");
|
|
64
|
+
const email = String(customer.email ?? "").toLowerCase().trim();
|
|
65
|
+
if (!email) throw new HttpError(400, "Customer email is required.", "invalid_payload");
|
|
66
|
+
|
|
67
|
+
const dupes = (await sr.entities["commerce.Customer"].filter({ email }, undefined, 2)) ?? [];
|
|
68
|
+
if (dupes.some((c: any) => c.id !== customer.id)) {
|
|
69
|
+
throw new HttpError(409, `A customer with email "${email}" already exists.`, "duplicate_email");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { id, created_date: _cd, updated_date: _ud, created_by: _cb, ...fields } = { ...customer, email };
|
|
73
|
+
let saved: any;
|
|
74
|
+
if (id) {
|
|
75
|
+
const prev = await getCustomer(sr, id);
|
|
76
|
+
await sr.entities["commerce.Customer"].update(id, fields);
|
|
77
|
+
saved = { ...prev, ...fields };
|
|
78
|
+
await dispatch(sr, "customer.updated", saved);
|
|
79
|
+
} else {
|
|
80
|
+
saved = await sr.entities["commerce.Customer"].create({
|
|
81
|
+
is_guest: fields.user_id ? false : (fields.is_guest ?? true),
|
|
82
|
+
orders_count: 0,
|
|
83
|
+
total_spent: 0,
|
|
84
|
+
is_paying_customer: false,
|
|
85
|
+
...fields,
|
|
86
|
+
});
|
|
87
|
+
await dispatch(sr, "customer.created", saved);
|
|
88
|
+
}
|
|
89
|
+
return saved;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function remove(sr: any, payload: any): Promise<any> {
|
|
93
|
+
const customer = await getCustomer(sr, payload.id);
|
|
94
|
+
if (payload.reassign_orders_to_guest !== false) {
|
|
95
|
+
const orders = await scanAll(sr.entities["commerce.Order"], { customer_id: customer.id }, "-created_date", { fields: ["id"] });
|
|
96
|
+
for (const o of orders) await sr.entities["commerce.Order"].update(o.id, { customer_id: "" });
|
|
97
|
+
}
|
|
98
|
+
await sr.entities["commerce.Customer"].delete(customer.id);
|
|
99
|
+
await dispatch(sr, "customer.deleted", customer);
|
|
100
|
+
return { deleted: customer.id };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Invite the customer to the app (creates/links a Base44 user, role "user"). */
|
|
104
|
+
async function invite(base44: any, sr: any, payload: any): Promise<any> {
|
|
105
|
+
const customer = await getCustomer(sr, payload.customer_id);
|
|
106
|
+
try {
|
|
107
|
+
const result = await base44.users.inviteUser(customer.email, "user");
|
|
108
|
+
const userId = result?.id ?? result?.user_id ?? "";
|
|
109
|
+
if (userId) {
|
|
110
|
+
await sr.entities["commerce.Customer"].update(customer.id, { user_id: userId, is_guest: false });
|
|
111
|
+
customer.user_id = userId;
|
|
112
|
+
customer.is_guest = false;
|
|
113
|
+
}
|
|
114
|
+
return { invited: true, customer };
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// invite may fail if the user already exists — surface but don't 500
|
|
117
|
+
return { invited: false, error: (e as Error).message, customer };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Recompute orders_count / total_spent / is_paying_customer.
|
|
123
|
+
* Counted spend = paid orders (date_paid set or processing/completed) minus
|
|
124
|
+
* their refunds; orders_count excludes cancelled/failed (standard commerce semantics).
|
|
125
|
+
*/
|
|
126
|
+
async function recalcStats(sr: any, customer: any): Promise<any> {
|
|
127
|
+
const orders = await scanAll(sr.entities["commerce.Order"], { customer_id: customer.id });
|
|
128
|
+
const counted = orders.filter((o) => !["cancelled", "failed"].includes(o.status));
|
|
129
|
+
const paid = orders.filter((o) => o.date_paid || ["processing", "completed"].includes(o.status));
|
|
130
|
+
const totalSpent = round2(paid.reduce(
|
|
131
|
+
(a, o) => a + (Number(o.total) || 0) - (Number(o.total_refunded) || 0),
|
|
132
|
+
0,
|
|
133
|
+
));
|
|
134
|
+
const patch = {
|
|
135
|
+
orders_count: counted.length,
|
|
136
|
+
total_spent: totalSpent,
|
|
137
|
+
is_paying_customer: totalSpent > 0,
|
|
138
|
+
};
|
|
139
|
+
await sr.entities["commerce.Customer"].update(customer.id, patch);
|
|
140
|
+
return { ...customer, ...patch };
|
|
141
|
+
}
|