@rovela-ai/sdk 0.6.0 → 0.7.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/dist/admin/api/discounts.d.ts +53 -0
- package/dist/admin/api/discounts.d.ts.map +1 -0
- package/dist/admin/api/discounts.js +325 -0
- package/dist/admin/api/discounts.js.map +1 -0
- package/dist/admin/api/index.d.ts +1 -0
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +2 -0
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/orders.js +1 -1
- package/dist/admin/api/orders.js.map +1 -1
- package/dist/admin/api/settings.d.ts.map +1 -1
- package/dist/admin/api/settings.js +10 -0
- package/dist/admin/api/settings.js.map +1 -1
- package/dist/admin/components/CustomerAddressPanel.d.ts.map +1 -1
- package/dist/admin/components/CustomerAddressPanel.js +13 -6
- package/dist/admin/components/CustomerAddressPanel.js.map +1 -1
- package/dist/admin/components/DiscountsManager.d.ts +5 -0
- package/dist/admin/components/DiscountsManager.d.ts.map +1 -0
- package/dist/admin/components/DiscountsManager.js +363 -0
- package/dist/admin/components/DiscountsManager.js.map +1 -0
- package/dist/admin/components/ShippingSettings.d.ts.map +1 -1
- package/dist/admin/components/ShippingSettings.js +19 -2
- package/dist/admin/components/ShippingSettings.js.map +1 -1
- package/dist/admin/components/StoreSettings.d.ts.map +1 -1
- package/dist/admin/components/StoreSettings.js +8 -1
- package/dist/admin/components/StoreSettings.js.map +1 -1
- package/dist/admin/components/TaxSettings.d.ts.map +1 -1
- package/dist/admin/components/TaxSettings.js +8 -3
- package/dist/admin/components/TaxSettings.js.map +1 -1
- package/dist/admin/components/index.d.ts +2 -0
- package/dist/admin/components/index.d.ts.map +1 -1
- package/dist/admin/components/index.js +1 -0
- package/dist/admin/components/index.js.map +1 -1
- package/dist/admin/hooks/index.d.ts +2 -0
- package/dist/admin/hooks/index.d.ts.map +1 -1
- package/dist/admin/hooks/index.js +1 -0
- package/dist/admin/hooks/index.js.map +1 -1
- package/dist/admin/hooks/useAdminDiscounts.d.ts +12 -0
- package/dist/admin/hooks/useAdminDiscounts.d.ts.map +1 -0
- package/dist/admin/hooks/useAdminDiscounts.js +77 -0
- package/dist/admin/hooks/useAdminDiscounts.js.map +1 -0
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +2 -2
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/types.d.ts +72 -0
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/auth/components/AddressForm.d.ts.map +1 -1
- package/dist/auth/components/AddressForm.js +22 -7
- package/dist/auth/components/AddressForm.js.map +1 -1
- package/dist/checkout/api/checkout.d.ts.map +1 -1
- package/dist/checkout/api/checkout.js +15 -2
- package/dist/checkout/api/checkout.js.map +1 -1
- package/dist/checkout/api/discount.d.ts +47 -0
- package/dist/checkout/api/discount.d.ts.map +1 -0
- package/dist/checkout/api/discount.js +77 -0
- package/dist/checkout/api/discount.js.map +1 -0
- package/dist/checkout/api/index.d.ts +1 -0
- package/dist/checkout/api/index.d.ts.map +1 -1
- package/dist/checkout/api/index.js +1 -0
- package/dist/checkout/api/index.js.map +1 -1
- package/dist/checkout/api/manual.d.ts.map +1 -1
- package/dist/checkout/api/manual.js +106 -155
- package/dist/checkout/api/manual.js.map +1 -1
- package/dist/checkout/api/shipping.d.ts.map +1 -1
- package/dist/checkout/api/shipping.js +19 -27
- package/dist/checkout/api/shipping.js.map +1 -1
- package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
- package/dist/checkout/components/CheckoutFlow.js +89 -9
- package/dist/checkout/components/CheckoutFlow.js.map +1 -1
- package/dist/checkout/components/OrderSummary.d.ts +1 -1
- package/dist/checkout/components/OrderSummary.d.ts.map +1 -1
- package/dist/checkout/components/OrderSummary.js +3 -3
- package/dist/checkout/components/OrderSummary.js.map +1 -1
- package/dist/checkout/components/ShippingForm.d.ts.map +1 -1
- package/dist/checkout/components/ShippingForm.js +42 -13
- package/dist/checkout/components/ShippingForm.js.map +1 -1
- package/dist/checkout/constants.d.ts +38 -0
- package/dist/checkout/constants.d.ts.map +1 -1
- package/dist/checkout/constants.js +39 -0
- package/dist/checkout/constants.js.map +1 -1
- package/dist/checkout/hooks/useCheckout.d.ts.map +1 -1
- package/dist/checkout/hooks/useCheckout.js +2 -0
- package/dist/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/checkout/server/checkout-customer.d.ts +30 -0
- package/dist/checkout/server/checkout-customer.d.ts.map +1 -0
- package/dist/checkout/server/checkout-customer.js +47 -0
- package/dist/checkout/server/checkout-customer.js.map +1 -0
- package/dist/checkout/server/create-checkout-session.d.ts.map +1 -1
- package/dist/checkout/server/create-checkout-session.js +149 -9
- package/dist/checkout/server/create-checkout-session.js.map +1 -1
- package/dist/checkout/server/discounts.d.ts +75 -0
- package/dist/checkout/server/discounts.d.ts.map +1 -0
- package/dist/checkout/server/discounts.js +294 -0
- package/dist/checkout/server/discounts.js.map +1 -0
- package/dist/checkout/server/handle-webhook.js +23 -1
- package/dist/checkout/server/handle-webhook.js.map +1 -1
- package/dist/checkout/server/index.d.ts +4 -0
- package/dist/checkout/server/index.d.ts.map +1 -1
- package/dist/checkout/server/index.js +4 -0
- package/dist/checkout/server/index.js.map +1 -1
- package/dist/checkout/server/order-service.d.ts.map +1 -1
- package/dist/checkout/server/order-service.js +60 -10
- package/dist/checkout/server/order-service.js.map +1 -1
- package/dist/checkout/server/pricing.d.ts +164 -0
- package/dist/checkout/server/pricing.d.ts.map +1 -0
- package/dist/checkout/server/pricing.js +309 -0
- package/dist/checkout/server/pricing.js.map +1 -0
- package/dist/checkout/types.d.ts +8 -0
- package/dist/checkout/types.d.ts.map +1 -1
- package/dist/core/StoreSettingsProvider.d.ts +4 -0
- package/dist/core/StoreSettingsProvider.d.ts.map +1 -1
- package/dist/core/StoreSettingsProvider.js +6 -0
- package/dist/core/StoreSettingsProvider.js.map +1 -1
- package/dist/core/api/settings.d.ts +1 -0
- package/dist/core/api/settings.d.ts.map +1 -1
- package/dist/core/api/settings.js +2 -0
- package/dist/core/api/settings.js.map +1 -1
- package/dist/core/db/queries.d.ts +249 -1
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +301 -1
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/db/schema.d.ts +618 -0
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +78 -0
- package/dist/core/db/schema.js.map +1 -1
- package/package.json +5 -1
package/dist/core/db/queries.js
CHANGED
|
@@ -849,6 +849,8 @@ export function periodBounds(periodDays) {
|
|
|
849
849
|
* selector); omit it for all-time behavior (backward-compatible).
|
|
850
850
|
*/
|
|
851
851
|
export async function getRecentOrders(limit = 5, periodDays) {
|
|
852
|
+
// Full orders SELECT enumerates payment/currency columns — self-heal first.
|
|
853
|
+
await ensurePaymentMethodsSchemaSafe();
|
|
852
854
|
const db = getDb();
|
|
853
855
|
const where = periodDays != null
|
|
854
856
|
? (() => {
|
|
@@ -1530,8 +1532,11 @@ export async function findCustomerGroups() {
|
|
|
1530
1532
|
sortOrder: schema.customerGroups.sortOrder,
|
|
1531
1533
|
createdAt: schema.customerGroups.createdAt,
|
|
1532
1534
|
updatedAt: schema.customerGroups.updatedAt,
|
|
1535
|
+
// Plain identifier (not a drizzle column interpolation): inside a raw
|
|
1536
|
+
// correlated subquery the interpolated form mis-renders and the count
|
|
1537
|
+
// silently returns 0. Outer table is unaliased `customer_groups`.
|
|
1533
1538
|
customerCount: sql `(
|
|
1534
|
-
SELECT count(*)::int FROM customers c WHERE c.group_id =
|
|
1539
|
+
SELECT count(*)::int FROM customers c WHERE c.group_id = customer_groups.id
|
|
1535
1540
|
)`,
|
|
1536
1541
|
})
|
|
1537
1542
|
.from(schema.customerGroups)
|
|
@@ -1642,6 +1647,9 @@ export async function ensurePaymentMethodsSchema() {
|
|
|
1642
1647
|
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_provider varchar(20) NOT NULL DEFAULT 'stripe'`);
|
|
1643
1648
|
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_method_id uuid`);
|
|
1644
1649
|
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_method_label varchar(100)`);
|
|
1650
|
+
// Per-order currency snapshot (SDK 0.7.0 — pricing core). Rides this
|
|
1651
|
+
// ensure because it already covers every order read/write chokepoint.
|
|
1652
|
+
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS currency varchar(3)`);
|
|
1645
1653
|
paymentMethodsSchemaEnsured = true;
|
|
1646
1654
|
}
|
|
1647
1655
|
catch (err) {
|
|
@@ -1740,6 +1748,256 @@ export async function deletePaymentMethod(id) {
|
|
|
1740
1748
|
return deleted.length > 0;
|
|
1741
1749
|
}
|
|
1742
1750
|
// =============================================================================
|
|
1751
|
+
// Discounts (native engine — see schema.ts for the rule model)
|
|
1752
|
+
// =============================================================================
|
|
1753
|
+
let discountsSchemaEnsured = false;
|
|
1754
|
+
/**
|
|
1755
|
+
* Self-heal the discounts schema on stores provisioned before the native
|
|
1756
|
+
* discounts engine (SDK 0.7.0). Idempotent + module-cached; called at every
|
|
1757
|
+
* discount read/write chokepoint.
|
|
1758
|
+
*/
|
|
1759
|
+
export async function ensureDiscountsSchema() {
|
|
1760
|
+
if (discountsSchemaEnsured)
|
|
1761
|
+
return;
|
|
1762
|
+
const db = getDb();
|
|
1763
|
+
try {
|
|
1764
|
+
await db.execute(sql `
|
|
1765
|
+
CREATE TABLE IF NOT EXISTS discounts (
|
|
1766
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1767
|
+
method varchar(20) NOT NULL DEFAULT 'code',
|
|
1768
|
+
code varchar(64),
|
|
1769
|
+
title varchar(255) NOT NULL,
|
|
1770
|
+
type varchar(20) NOT NULL,
|
|
1771
|
+
value decimal(10,2) NOT NULL DEFAULT '0',
|
|
1772
|
+
applies_to varchar(20) NOT NULL DEFAULT 'order',
|
|
1773
|
+
target_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
1774
|
+
min_subtotal decimal(10,2),
|
|
1775
|
+
min_quantity integer,
|
|
1776
|
+
eligibility varchar(20) NOT NULL DEFAULT 'all',
|
|
1777
|
+
group_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
1778
|
+
usage_limit integer,
|
|
1779
|
+
once_per_customer boolean NOT NULL DEFAULT false,
|
|
1780
|
+
used_count integer NOT NULL DEFAULT 0,
|
|
1781
|
+
combines_with jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
1782
|
+
bxgy jsonb,
|
|
1783
|
+
starts_at timestamp,
|
|
1784
|
+
ends_at timestamp,
|
|
1785
|
+
enabled boolean NOT NULL DEFAULT true,
|
|
1786
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
1787
|
+
created_at timestamp NOT NULL DEFAULT now(),
|
|
1788
|
+
updated_at timestamp NOT NULL DEFAULT now()
|
|
1789
|
+
)
|
|
1790
|
+
`);
|
|
1791
|
+
await db.execute(sql `CREATE INDEX IF NOT EXISTS discounts_code_idx ON discounts (code)`);
|
|
1792
|
+
await db.execute(sql `CREATE INDEX IF NOT EXISTS discounts_enabled_idx ON discounts (enabled)`);
|
|
1793
|
+
await db.execute(sql `CREATE INDEX IF NOT EXISTS discounts_method_idx ON discounts (method)`);
|
|
1794
|
+
// Case-insensitive code uniqueness (codes are stored UPPERCASE; the
|
|
1795
|
+
// partial unique index is belt + suspenders against write-path drift).
|
|
1796
|
+
await db.execute(sql `CREATE UNIQUE INDEX IF NOT EXISTS discounts_code_unique_idx
|
|
1797
|
+
ON discounts (upper(code)) WHERE code IS NOT NULL`);
|
|
1798
|
+
await db.execute(sql `
|
|
1799
|
+
CREATE TABLE IF NOT EXISTS discount_redemptions (
|
|
1800
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1801
|
+
discount_id uuid NOT NULL,
|
|
1802
|
+
order_id uuid NOT NULL,
|
|
1803
|
+
customer_id uuid,
|
|
1804
|
+
email varchar(255),
|
|
1805
|
+
amount decimal(10,2) NOT NULL DEFAULT '0',
|
|
1806
|
+
created_at timestamp NOT NULL DEFAULT now()
|
|
1807
|
+
)
|
|
1808
|
+
`);
|
|
1809
|
+
await db.execute(sql `CREATE INDEX IF NOT EXISTS discount_redemptions_discount_idx ON discount_redemptions (discount_id)`);
|
|
1810
|
+
await db.execute(sql `CREATE INDEX IF NOT EXISTS discount_redemptions_customer_idx ON discount_redemptions (discount_id, customer_id)`);
|
|
1811
|
+
await db.execute(sql `CREATE INDEX IF NOT EXISTS discount_redemptions_email_idx ON discount_redemptions (discount_id, email)`);
|
|
1812
|
+
await db.execute(sql `CREATE INDEX IF NOT EXISTS discount_redemptions_order_idx ON discount_redemptions (order_id)`);
|
|
1813
|
+
discountsSchemaEnsured = true;
|
|
1814
|
+
}
|
|
1815
|
+
catch (err) {
|
|
1816
|
+
console.error('[ensureDiscountsSchema] migration failed:', err);
|
|
1817
|
+
throw err;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
/** Best-effort variant — never throws (checkout must not 500 on a migration hiccup). */
|
|
1821
|
+
export async function ensureDiscountsSchemaSafe() {
|
|
1822
|
+
try {
|
|
1823
|
+
await ensureDiscountsSchema();
|
|
1824
|
+
}
|
|
1825
|
+
catch {
|
|
1826
|
+
// logged inside ensureDiscountsSchema
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
/** All discounts, admin view (sorted). */
|
|
1830
|
+
export async function findDiscounts() {
|
|
1831
|
+
await ensureDiscountsSchema();
|
|
1832
|
+
const db = getDb();
|
|
1833
|
+
return db
|
|
1834
|
+
.select()
|
|
1835
|
+
.from(schema.discounts)
|
|
1836
|
+
.orderBy(asc(schema.discounts.sortOrder), desc(schema.discounts.createdAt));
|
|
1837
|
+
}
|
|
1838
|
+
export async function findDiscountById(id) {
|
|
1839
|
+
await ensureDiscountsSchema();
|
|
1840
|
+
const db = getDb();
|
|
1841
|
+
const rows = await db
|
|
1842
|
+
.select()
|
|
1843
|
+
.from(schema.discounts)
|
|
1844
|
+
.where(eq(schema.discounts.id, id))
|
|
1845
|
+
.limit(1);
|
|
1846
|
+
return rows[0] ?? null;
|
|
1847
|
+
}
|
|
1848
|
+
/** Enabled code discount by (case-insensitive) code — checkout entry point. */
|
|
1849
|
+
export async function findDiscountByCode(code) {
|
|
1850
|
+
await ensureDiscountsSchemaSafe();
|
|
1851
|
+
const db = getDb();
|
|
1852
|
+
const normalized = code.trim().toUpperCase();
|
|
1853
|
+
if (!normalized)
|
|
1854
|
+
return null;
|
|
1855
|
+
const rows = await db
|
|
1856
|
+
.select()
|
|
1857
|
+
.from(schema.discounts)
|
|
1858
|
+
.where(and(eq(schema.discounts.method, 'code'), sql `upper(${schema.discounts.code}) = ${normalized}`))
|
|
1859
|
+
.limit(1);
|
|
1860
|
+
return rows[0] ?? null;
|
|
1861
|
+
}
|
|
1862
|
+
/** Enabled automatic discounts — always evaluated at checkout. */
|
|
1863
|
+
export async function findActiveAutomaticDiscounts() {
|
|
1864
|
+
await ensureDiscountsSchemaSafe();
|
|
1865
|
+
const db = getDb();
|
|
1866
|
+
return db
|
|
1867
|
+
.select()
|
|
1868
|
+
.from(schema.discounts)
|
|
1869
|
+
.where(and(eq(schema.discounts.method, 'automatic'), eq(schema.discounts.enabled, true)))
|
|
1870
|
+
.orderBy(asc(schema.discounts.sortOrder), asc(schema.discounts.createdAt));
|
|
1871
|
+
}
|
|
1872
|
+
export async function createDiscount(data) {
|
|
1873
|
+
await ensureDiscountsSchema();
|
|
1874
|
+
const db = getDb();
|
|
1875
|
+
const [row] = await db
|
|
1876
|
+
.insert(schema.discounts)
|
|
1877
|
+
.values({
|
|
1878
|
+
method: data.method ?? 'code',
|
|
1879
|
+
code: data.code ? data.code.trim().toUpperCase() : null,
|
|
1880
|
+
title: data.title,
|
|
1881
|
+
type: data.type,
|
|
1882
|
+
value: String(data.value ?? 0),
|
|
1883
|
+
appliesTo: data.appliesTo ?? 'order',
|
|
1884
|
+
targetIds: data.targetIds ?? [],
|
|
1885
|
+
minSubtotal: data.minSubtotal != null ? String(data.minSubtotal) : null,
|
|
1886
|
+
minQuantity: data.minQuantity ?? null,
|
|
1887
|
+
eligibility: data.eligibility ?? 'all',
|
|
1888
|
+
groupIds: data.groupIds ?? [],
|
|
1889
|
+
usageLimit: data.usageLimit ?? null,
|
|
1890
|
+
oncePerCustomer: data.oncePerCustomer ?? false,
|
|
1891
|
+
combinesWith: data.combinesWith ?? {},
|
|
1892
|
+
bxgy: data.bxgy ?? null,
|
|
1893
|
+
startsAt: data.startsAt ?? null,
|
|
1894
|
+
endsAt: data.endsAt ?? null,
|
|
1895
|
+
enabled: data.enabled ?? true,
|
|
1896
|
+
sortOrder: data.sortOrder ?? 0,
|
|
1897
|
+
})
|
|
1898
|
+
.returning();
|
|
1899
|
+
return row;
|
|
1900
|
+
}
|
|
1901
|
+
export async function updateDiscount(id, data) {
|
|
1902
|
+
await ensureDiscountsSchema();
|
|
1903
|
+
const db = getDb();
|
|
1904
|
+
const [row] = await db
|
|
1905
|
+
.update(schema.discounts)
|
|
1906
|
+
.set({
|
|
1907
|
+
...(data.method !== undefined && { method: data.method }),
|
|
1908
|
+
...(data.code !== undefined && {
|
|
1909
|
+
code: data.code ? data.code.trim().toUpperCase() : null,
|
|
1910
|
+
}),
|
|
1911
|
+
...(data.title !== undefined && { title: data.title }),
|
|
1912
|
+
...(data.type !== undefined && { type: data.type }),
|
|
1913
|
+
...(data.value !== undefined && { value: String(data.value ?? 0) }),
|
|
1914
|
+
...(data.appliesTo !== undefined && { appliesTo: data.appliesTo }),
|
|
1915
|
+
...(data.targetIds !== undefined && { targetIds: data.targetIds ?? [] }),
|
|
1916
|
+
...(data.minSubtotal !== undefined && {
|
|
1917
|
+
minSubtotal: data.minSubtotal != null ? String(data.minSubtotal) : null,
|
|
1918
|
+
}),
|
|
1919
|
+
...(data.minQuantity !== undefined && { minQuantity: data.minQuantity ?? null }),
|
|
1920
|
+
...(data.eligibility !== undefined && { eligibility: data.eligibility }),
|
|
1921
|
+
...(data.groupIds !== undefined && { groupIds: data.groupIds ?? [] }),
|
|
1922
|
+
...(data.usageLimit !== undefined && { usageLimit: data.usageLimit ?? null }),
|
|
1923
|
+
...(data.oncePerCustomer !== undefined && { oncePerCustomer: data.oncePerCustomer }),
|
|
1924
|
+
...(data.combinesWith !== undefined && { combinesWith: data.combinesWith ?? {} }),
|
|
1925
|
+
...(data.bxgy !== undefined && { bxgy: data.bxgy ?? null }),
|
|
1926
|
+
...(data.startsAt !== undefined && { startsAt: data.startsAt ?? null }),
|
|
1927
|
+
...(data.endsAt !== undefined && { endsAt: data.endsAt ?? null }),
|
|
1928
|
+
...(data.enabled !== undefined && { enabled: data.enabled }),
|
|
1929
|
+
...(data.sortOrder !== undefined && { sortOrder: data.sortOrder }),
|
|
1930
|
+
updatedAt: new Date(),
|
|
1931
|
+
})
|
|
1932
|
+
.where(eq(schema.discounts.id, id))
|
|
1933
|
+
.returning();
|
|
1934
|
+
return row ?? null;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Hard delete. Redemption history survives (soft refs) so past orders keep
|
|
1938
|
+
* their recorded discount code + amount snapshots.
|
|
1939
|
+
*/
|
|
1940
|
+
export async function deleteDiscount(id) {
|
|
1941
|
+
await ensureDiscountsSchema();
|
|
1942
|
+
const db = getDb();
|
|
1943
|
+
const deleted = await db
|
|
1944
|
+
.delete(schema.discounts)
|
|
1945
|
+
.where(eq(schema.discounts.id, id))
|
|
1946
|
+
.returning({ id: schema.discounts.id });
|
|
1947
|
+
return deleted.length > 0;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Prior redemptions by this customer (id OR email) — powers oncePerCustomer.
|
|
1951
|
+
*/
|
|
1952
|
+
export async function countDiscountRedemptionsForCustomer(discountId, customer) {
|
|
1953
|
+
await ensureDiscountsSchemaSafe();
|
|
1954
|
+
const db = getDb();
|
|
1955
|
+
const conditions = [];
|
|
1956
|
+
if (customer.customerId) {
|
|
1957
|
+
conditions.push(eq(schema.discountRedemptions.customerId, customer.customerId));
|
|
1958
|
+
}
|
|
1959
|
+
if (customer.email) {
|
|
1960
|
+
conditions.push(sql `lower(${schema.discountRedemptions.email}) = ${customer.email.toLowerCase()}`);
|
|
1961
|
+
}
|
|
1962
|
+
if (conditions.length === 0)
|
|
1963
|
+
return 0;
|
|
1964
|
+
const rows = await db
|
|
1965
|
+
.select({ count: sql `count(*)` })
|
|
1966
|
+
.from(schema.discountRedemptions)
|
|
1967
|
+
.where(and(eq(schema.discountRedemptions.discountId, discountId), or(...conditions)));
|
|
1968
|
+
return Number(rows[0]?.count || 0);
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Atomically claim one use of a discount (usage-limit guard) and record the
|
|
1972
|
+
* redemption. Returns false when the limit was exhausted between evaluation
|
|
1973
|
+
* and order creation — the order still stands (the race window is seconds);
|
|
1974
|
+
* the claim failure is logged for reconciliation.
|
|
1975
|
+
*/
|
|
1976
|
+
export async function recordDiscountRedemption(params) {
|
|
1977
|
+
await ensureDiscountsSchemaSafe();
|
|
1978
|
+
const db = getDb();
|
|
1979
|
+
const claimed = await db.execute(sql `
|
|
1980
|
+
UPDATE discounts
|
|
1981
|
+
SET used_count = used_count + 1, updated_at = now()
|
|
1982
|
+
WHERE id = ${params.discountId}
|
|
1983
|
+
AND (usage_limit IS NULL OR used_count < usage_limit)
|
|
1984
|
+
RETURNING id
|
|
1985
|
+
`);
|
|
1986
|
+
const claimedRows = claimed.rows ?? [];
|
|
1987
|
+
const ok = Array.isArray(claimedRows) ? claimedRows.length > 0 : false;
|
|
1988
|
+
await db.insert(schema.discountRedemptions).values({
|
|
1989
|
+
discountId: params.discountId,
|
|
1990
|
+
orderId: params.orderId,
|
|
1991
|
+
customerId: params.customerId ?? null,
|
|
1992
|
+
email: params.email ? params.email.toLowerCase() : null,
|
|
1993
|
+
amount: (params.amountCents / 100).toFixed(2),
|
|
1994
|
+
});
|
|
1995
|
+
if (!ok) {
|
|
1996
|
+
console.warn('[Discounts] usage-limit claim failed post-order (limit hit in the race window):', params.discountId, 'order:', params.orderId);
|
|
1997
|
+
}
|
|
1998
|
+
return ok;
|
|
1999
|
+
}
|
|
2000
|
+
// =============================================================================
|
|
1743
2001
|
// Order Queries (for admin module)
|
|
1744
2002
|
// =============================================================================
|
|
1745
2003
|
/**
|
|
@@ -1829,12 +2087,50 @@ export async function getOrdersByStatus(periodDays) {
|
|
|
1829
2087
|
count: Number(r.count),
|
|
1830
2088
|
}));
|
|
1831
2089
|
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Self-heal for store_settings columns added after a store's branch was
|
|
2092
|
+
* created (drizzle `.select()` enumerates every schema column, so a missing
|
|
2093
|
+
* column breaks ALL settings reads). Idempotent; cached in-process. Called
|
|
2094
|
+
* best-effort at the top of findSettings/upsertSettings — the settings
|
|
2095
|
+
* chokepoints every surface goes through.
|
|
2096
|
+
*
|
|
2097
|
+
* Columns owned here:
|
|
2098
|
+
* - store_country varchar(2) — SDK 0.7.0 (store-country single source of truth)
|
|
2099
|
+
*/
|
|
2100
|
+
let storeSettingsSchemaEnsured = false;
|
|
2101
|
+
export async function ensureStoreSettingsSchema() {
|
|
2102
|
+
if (storeSettingsSchemaEnsured)
|
|
2103
|
+
return;
|
|
2104
|
+
const db = getDb();
|
|
2105
|
+
try {
|
|
2106
|
+
await db.execute(sql `
|
|
2107
|
+
ALTER TABLE store_settings
|
|
2108
|
+
ADD COLUMN IF NOT EXISTS store_country varchar(2)
|
|
2109
|
+
`);
|
|
2110
|
+
storeSettingsSchemaEnsured = true;
|
|
2111
|
+
}
|
|
2112
|
+
catch (err) {
|
|
2113
|
+
// Don't poison the cache on transient failures — retry next call.
|
|
2114
|
+
console.error('[ensureStoreSettingsSchema] migration failed:', err);
|
|
2115
|
+
throw err;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
/** Best-effort variant — never throws (the subsequent read surfaces real errors). */
|
|
2119
|
+
export async function ensureStoreSettingsSchemaSafe() {
|
|
2120
|
+
try {
|
|
2121
|
+
await ensureStoreSettingsSchema();
|
|
2122
|
+
}
|
|
2123
|
+
catch {
|
|
2124
|
+
// logged inside ensureStoreSettingsSchema
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
1832
2127
|
/**
|
|
1833
2128
|
* Find store settings.
|
|
1834
2129
|
* Returns null if no settings exist (first time access).
|
|
1835
2130
|
* Each store has only one settings row.
|
|
1836
2131
|
*/
|
|
1837
2132
|
export async function findSettings() {
|
|
2133
|
+
await ensureStoreSettingsSchemaSafe();
|
|
1838
2134
|
const db = getDb();
|
|
1839
2135
|
const [settings] = await db
|
|
1840
2136
|
.select()
|
|
@@ -1848,6 +2144,7 @@ export async function findSettings() {
|
|
|
1848
2144
|
logoUrl: settings.logoUrl,
|
|
1849
2145
|
storeCurrency: settings.storeCurrency,
|
|
1850
2146
|
storeTimezone: settings.storeTimezone,
|
|
2147
|
+
storeCountry: settings.storeCountry,
|
|
1851
2148
|
taxIncludedInPrices: settings.taxIncludedInPrices,
|
|
1852
2149
|
shippingEnabled: settings.shippingEnabled,
|
|
1853
2150
|
freeShippingThreshold: settings.freeShippingThreshold
|
|
@@ -1902,6 +2199,7 @@ export async function listMetafieldKeys() {
|
|
|
1902
2199
|
* Each store has only one settings row.
|
|
1903
2200
|
*/
|
|
1904
2201
|
export async function upsertSettings(data) {
|
|
2202
|
+
await ensureStoreSettingsSchemaSafe();
|
|
1905
2203
|
const db = getDb();
|
|
1906
2204
|
// Check if settings exist
|
|
1907
2205
|
const existing = await db
|
|
@@ -1914,6 +2212,7 @@ export async function upsertSettings(data) {
|
|
|
1914
2212
|
logoUrl: data.logoUrl ?? undefined,
|
|
1915
2213
|
storeCurrency: data.storeCurrency ?? undefined,
|
|
1916
2214
|
storeTimezone: data.storeTimezone ?? undefined,
|
|
2215
|
+
storeCountry: data.storeCountry ?? undefined,
|
|
1917
2216
|
taxIncludedInPrices: data.taxIncludedInPrices ?? undefined,
|
|
1918
2217
|
shippingEnabled: data.shippingEnabled ?? undefined,
|
|
1919
2218
|
freeShippingThreshold: data.freeShippingThreshold !== null && data.freeShippingThreshold !== undefined
|
|
@@ -1959,6 +2258,7 @@ export async function upsertSettings(data) {
|
|
|
1959
2258
|
logoUrl: result.logoUrl,
|
|
1960
2259
|
storeCurrency: result.storeCurrency,
|
|
1961
2260
|
storeTimezone: result.storeTimezone,
|
|
2261
|
+
storeCountry: result.storeCountry,
|
|
1962
2262
|
taxIncludedInPrices: result.taxIncludedInPrices,
|
|
1963
2263
|
shippingEnabled: result.shippingEnabled,
|
|
1964
2264
|
freeShippingThreshold: result.freeShippingThreshold
|