@rovela-ai/sdk 0.21.0 → 0.23.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/email-sending.d.ts +24 -0
- package/dist/admin/api/email-sending.d.ts.map +1 -0
- package/dist/admin/api/email-sending.js +155 -0
- package/dist/admin/api/email-sending.js.map +1 -0
- package/dist/admin/api/index.d.ts +2 -0
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +4 -0
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/integrations-brevo.d.ts +24 -0
- package/dist/admin/api/integrations-brevo.d.ts.map +1 -0
- package/dist/admin/api/integrations-brevo.js +286 -0
- package/dist/admin/api/integrations-brevo.js.map +1 -0
- package/dist/admin/api/integrations.d.ts +2 -0
- package/dist/admin/api/integrations.d.ts.map +1 -1
- package/dist/admin/api/integrations.js +1 -0
- package/dist/admin/api/integrations.js.map +1 -1
- package/dist/admin/api/products.d.ts.map +1 -1
- package/dist/admin/api/products.js +4 -0
- package/dist/admin/api/products.js.map +1 -1
- package/dist/admin/api/settings.d.ts.map +1 -1
- package/dist/admin/api/settings.js +4 -1
- package/dist/admin/api/settings.js.map +1 -1
- package/dist/admin/api/tax-zones.d.ts.map +1 -1
- package/dist/admin/api/tax-zones.js +7 -0
- package/dist/admin/api/tax-zones.js.map +1 -1
- package/dist/admin/components/EmailsManager.d.ts.map +1 -1
- package/dist/admin/components/EmailsManager.js +2 -1
- package/dist/admin/components/EmailsManager.js.map +1 -1
- package/dist/admin/components/IntegrationsManager.d.ts.map +1 -1
- package/dist/admin/components/IntegrationsManager.js +98 -2
- package/dist/admin/components/IntegrationsManager.js.map +1 -1
- package/dist/admin/components/OrderDetails.d.ts.map +1 -1
- package/dist/admin/components/OrderDetails.js +5 -1
- package/dist/admin/components/OrderDetails.js.map +1 -1
- package/dist/admin/components/ProductForm.d.ts.map +1 -1
- package/dist/admin/components/ProductForm.js +36 -1
- package/dist/admin/components/ProductForm.js.map +1 -1
- package/dist/admin/components/TaxSettings.d.ts.map +1 -1
- package/dist/admin/components/TaxSettings.js +16 -4
- package/dist/admin/components/TaxSettings.js.map +1 -1
- package/dist/admin/components/emails/EmailSendingPanel.d.ts +2 -0
- package/dist/admin/components/emails/EmailSendingPanel.d.ts.map +1 -0
- package/dist/admin/components/emails/EmailSendingPanel.js +131 -0
- package/dist/admin/components/emails/EmailSendingPanel.js.map +1 -0
- package/dist/admin/integrations/registry.d.ts +2 -0
- package/dist/admin/integrations/registry.d.ts.map +1 -1
- package/dist/admin/integrations/registry.js +41 -0
- package/dist/admin/integrations/registry.js.map +1 -1
- package/dist/admin/types.d.ts +15 -0
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/checkout/api/manual.d.ts.map +1 -1
- package/dist/checkout/api/manual.js +7 -0
- package/dist/checkout/api/manual.js.map +1 -1
- package/dist/checkout/api/shipping.d.ts +0 -18
- package/dist/checkout/api/shipping.d.ts.map +1 -1
- package/dist/checkout/api/shipping.js +33 -3
- 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 +30 -13
- 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 +5 -2
- package/dist/checkout/components/OrderSummary.js.map +1 -1
- package/dist/checkout/server/create-checkout-session.d.ts.map +1 -1
- package/dist/checkout/server/create-checkout-session.js +38 -9
- package/dist/checkout/server/create-checkout-session.js.map +1 -1
- package/dist/checkout/server/handle-webhook.js +6 -0
- package/dist/checkout/server/handle-webhook.js.map +1 -1
- package/dist/checkout/server/order-service.d.ts.map +1 -1
- package/dist/checkout/server/order-service.js +21 -0
- package/dist/checkout/server/order-service.js.map +1 -1
- package/dist/checkout/server/pricing.d.ts +28 -1
- package/dist/checkout/server/pricing.d.ts.map +1 -1
- package/dist/checkout/server/pricing.js +84 -6
- package/dist/checkout/server/pricing.js.map +1 -1
- package/dist/checkout/types.d.ts +32 -0
- package/dist/checkout/types.d.ts.map +1 -1
- package/dist/core/db/queries.d.ts +114 -25
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +124 -2
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/db/schema.d.ts +213 -0
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +42 -0
- package/dist/core/db/schema.js.map +1 -1
- package/dist/emails/index.d.ts +2 -0
- package/dist/emails/index.d.ts.map +1 -1
- package/dist/emails/index.js +1 -0
- package/dist/emails/index.js.map +1 -1
- package/dist/emails/send/orders.d.ts +4 -0
- package/dist/emails/send/orders.d.ts.map +1 -1
- package/dist/emails/send/orders.js +1 -0
- package/dist/emails/send/orders.js.map +1 -1
- package/dist/emails/sender.d.ts.map +1 -1
- package/dist/emails/sender.js +30 -5
- package/dist/emails/sender.js.map +1 -1
- package/dist/emails/templates/order-confirmation.d.ts +9 -0
- package/dist/emails/templates/order-confirmation.d.ts.map +1 -1
- package/dist/emails/templates/order-confirmation.js +19 -8
- package/dist/emails/templates/order-confirmation.js.map +1 -1
- package/dist/emails/transport-smtp.d.ts +58 -0
- package/dist/emails/transport-smtp.d.ts.map +1 -0
- package/dist/emails/transport-smtp.js +97 -0
- package/dist/emails/transport-smtp.js.map +1 -0
- package/dist/emails/transport.d.ts +77 -0
- package/dist/emails/transport.d.ts.map +1 -0
- package/dist/emails/transport.js +154 -0
- package/dist/emails/transport.js.map +1 -0
- package/dist/emails/types.d.ts +5 -0
- package/dist/emails/types.d.ts.map +1 -1
- package/dist/products/api/products.d.ts.map +1 -1
- package/dist/products/api/products.js +18 -3
- package/dist/products/api/products.js.map +1 -1
- package/docs/changelog/epoch-09-catalog-tax.md +91 -0
- package/docs/changelog/epoch-10-email-transport.md +97 -0
- package/package.json +2 -1
- package/templates/store-template/.claude/skills/manage-products/SKILL.md +1 -1
- package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +19 -4
- package/templates/store-template/app/api/admin/emails/sending/route.ts +9 -0
- package/templates/store-template/app/api/admin/integrations/brevo/backfill/route.ts +9 -0
- package/templates/store-template/app/api/admin/integrations/brevo/route.ts +9 -0
- package/templates/store-template/next.config.js +4 -1
- package/templates/store-template/package.json +2 -1
package/dist/core/db/queries.js
CHANGED
|
@@ -37,6 +37,9 @@ export async function ensureCatalogTranslationsSchema() {
|
|
|
37
37
|
`);
|
|
38
38
|
await db.execute(sql `
|
|
39
39
|
ALTER TABLE products ADD COLUMN IF NOT EXISTS metafields jsonb NOT NULL DEFAULT '[]'::jsonb
|
|
40
|
+
`);
|
|
41
|
+
await db.execute(sql `
|
|
42
|
+
ALTER TABLE products ADD COLUMN IF NOT EXISTS tax_class varchar(50)
|
|
40
43
|
`);
|
|
41
44
|
catalogTranslationsEnsured = true;
|
|
42
45
|
}
|
|
@@ -59,7 +62,10 @@ export async function findProducts(options = {}) {
|
|
|
59
62
|
const db = getDb();
|
|
60
63
|
const conditions = [];
|
|
61
64
|
if (options.categoryId) {
|
|
62
|
-
|
|
65
|
+
const categoryIds = options.categoryId;
|
|
66
|
+
conditions.push(Array.isArray(categoryIds)
|
|
67
|
+
? inArray(schema.products.categoryId, categoryIds)
|
|
68
|
+
: eq(schema.products.categoryId, categoryIds));
|
|
63
69
|
}
|
|
64
70
|
if (options.status) {
|
|
65
71
|
conditions.push(eq(schema.products.status, options.status));
|
|
@@ -156,7 +162,10 @@ export async function countProducts(options = {}) {
|
|
|
156
162
|
const db = getDb();
|
|
157
163
|
const conditions = [];
|
|
158
164
|
if (options.categoryId) {
|
|
159
|
-
|
|
165
|
+
const categoryIds = options.categoryId;
|
|
166
|
+
conditions.push(Array.isArray(categoryIds)
|
|
167
|
+
? inArray(schema.products.categoryId, categoryIds)
|
|
168
|
+
: eq(schema.products.categoryId, categoryIds));
|
|
160
169
|
}
|
|
161
170
|
if (options.status) {
|
|
162
171
|
conditions.push(eq(schema.products.status, options.status));
|
|
@@ -429,6 +438,51 @@ export async function findCategories() {
|
|
|
429
438
|
.from(schema.categories)
|
|
430
439
|
.orderBy(asc(schema.categories.order), asc(schema.categories.name));
|
|
431
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* productId → tax_class for a set of products, one query. Used by the
|
|
443
|
+
* pre-checkout estimate endpoint so its per-class tax preview matches the
|
|
444
|
+
* final quote. Unknown ids are simply absent from the map.
|
|
445
|
+
*/
|
|
446
|
+
export async function findTaxClassesByProductIds(productIds) {
|
|
447
|
+
if (productIds.length === 0)
|
|
448
|
+
return {};
|
|
449
|
+
await ensureCatalogTranslationsSchemaSafe();
|
|
450
|
+
const db = getDb();
|
|
451
|
+
const rows = await db
|
|
452
|
+
.select({ id: schema.products.id, taxClass: schema.products.taxClass })
|
|
453
|
+
.from(schema.products)
|
|
454
|
+
.where(inArray(schema.products.id, productIds));
|
|
455
|
+
const out = {};
|
|
456
|
+
for (const row of rows)
|
|
457
|
+
out[row.id] = row.taxClass ?? null;
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* All descendant category ids of a category (children, grandchildren, …),
|
|
462
|
+
* excluding the category itself. Recursive CTE over categories.parent_id —
|
|
463
|
+
* correct at any depth, one round trip. Powers the tree-aware public catalog
|
|
464
|
+
* filter: a parent-category link shows its leaves' products (imported
|
|
465
|
+
* PrestaShop catalogs assign products to LEAF categories).
|
|
466
|
+
*/
|
|
467
|
+
export async function findCategoryDescendantIds(categoryId) {
|
|
468
|
+
const db = getDb();
|
|
469
|
+
const result = await db.execute(sql `
|
|
470
|
+
WITH RECURSIVE descendants AS (
|
|
471
|
+
SELECT id FROM categories WHERE parent_id = ${categoryId}
|
|
472
|
+
UNION ALL
|
|
473
|
+
SELECT c.id FROM categories c
|
|
474
|
+
INNER JOIN descendants d ON c.parent_id = d.id
|
|
475
|
+
)
|
|
476
|
+
SELECT id FROM descendants
|
|
477
|
+
`);
|
|
478
|
+
// Driver-shape hedge (same as the redemption claim below): neon-http returns
|
|
479
|
+
// { rows } on current drizzle, a bare array on older ones.
|
|
480
|
+
const raw = result;
|
|
481
|
+
const rows = Array.isArray(raw)
|
|
482
|
+
? raw
|
|
483
|
+
: (raw.rows ?? []);
|
|
484
|
+
return rows.map((r) => String(r.id));
|
|
485
|
+
}
|
|
432
486
|
/**
|
|
433
487
|
* Find a category by slug
|
|
434
488
|
*/
|
|
@@ -1825,6 +1879,8 @@ export async function ensureVisibilityColumns() {
|
|
|
1825
1879
|
await db.execute(sql `ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS visible_to_groups jsonb NOT NULL DEFAULT '[]'::jsonb`);
|
|
1826
1880
|
await db.execute(sql `ALTER TABLE shipping_rates ADD COLUMN IF NOT EXISTS visible_to_groups jsonb NOT NULL DEFAULT '[]'::jsonb`);
|
|
1827
1881
|
await db.execute(sql `ALTER TABLE tax_zones ADD COLUMN IF NOT EXISTS visible_to_groups jsonb NOT NULL DEFAULT '[]'::jsonb`);
|
|
1882
|
+
// Per-class rates (0.22.0) ride the same tax-zone chokepoints.
|
|
1883
|
+
await db.execute(sql `ALTER TABLE tax_zones ADD COLUMN IF NOT EXISTS class_rates jsonb NOT NULL DEFAULT '{}'::jsonb`);
|
|
1828
1884
|
visibilityColumnsEnsured = true;
|
|
1829
1885
|
}
|
|
1830
1886
|
catch (err) {
|
|
@@ -1882,6 +1938,7 @@ export async function ensurePaymentMethodsSchema() {
|
|
|
1882
1938
|
await db.execute(sql `CREATE INDEX IF NOT EXISTS payment_methods_sort_order_idx ON payment_methods (sort_order)`);
|
|
1883
1939
|
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_provider varchar(20) NOT NULL DEFAULT 'stripe'`);
|
|
1884
1940
|
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_method_id uuid`);
|
|
1941
|
+
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS tax_breakdown jsonb`);
|
|
1885
1942
|
await db.execute(sql `ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_method_label varchar(100)`);
|
|
1886
1943
|
// Per-order currency snapshot (SDK 0.7.0 — pricing core). Rides this
|
|
1887
1944
|
// ensure because it already covers every order read/write chokepoint.
|
|
@@ -2550,6 +2607,20 @@ export async function ensureStoreSettingsSchema() {
|
|
|
2550
2607
|
ADD COLUMN IF NOT EXISTS colissimo_enabled boolean DEFAULT false,
|
|
2551
2608
|
ADD COLUMN IF NOT EXISTS colissimo_sender_address jsonb,
|
|
2552
2609
|
ADD COLUMN IF NOT EXISTS colissimo_default_weight_kg decimal(8,2)
|
|
2610
|
+
`);
|
|
2611
|
+
// Transactional-email transport (SDK 0.23.0). NULL email_transport =
|
|
2612
|
+
// 'platform' (today's Resend path) — the whole fleet keeps its exact
|
|
2613
|
+
// behavior until a merchant opts in. smtp_password is server-only.
|
|
2614
|
+
await db.execute(sql `
|
|
2615
|
+
ALTER TABLE store_settings
|
|
2616
|
+
ADD COLUMN IF NOT EXISTS email_transport varchar(20),
|
|
2617
|
+
ADD COLUMN IF NOT EXISTS email_from_address varchar(255),
|
|
2618
|
+
ADD COLUMN IF NOT EXISTS email_from_name varchar(120),
|
|
2619
|
+
ADD COLUMN IF NOT EXISTS smtp_host varchar(255),
|
|
2620
|
+
ADD COLUMN IF NOT EXISTS smtp_port integer,
|
|
2621
|
+
ADD COLUMN IF NOT EXISTS smtp_secure boolean,
|
|
2622
|
+
ADD COLUMN IF NOT EXISTS smtp_user varchar(255),
|
|
2623
|
+
ADD COLUMN IF NOT EXISTS smtp_password varchar(255)
|
|
2553
2624
|
`);
|
|
2554
2625
|
storeSettingsSchemaEnsured = true;
|
|
2555
2626
|
}
|
|
@@ -2753,6 +2824,15 @@ export async function findSettings() {
|
|
|
2753
2824
|
colissimoDefaultWeightKg: settings.colissimoDefaultWeightKg
|
|
2754
2825
|
? Number(settings.colissimoDefaultWeightKg)
|
|
2755
2826
|
: null,
|
|
2827
|
+
// Transactional-email transport (SDK 0.23.0)
|
|
2828
|
+
emailTransport: settings.emailTransport ?? null,
|
|
2829
|
+
emailFromAddress: settings.emailFromAddress,
|
|
2830
|
+
emailFromName: settings.emailFromName,
|
|
2831
|
+
smtpHost: settings.smtpHost,
|
|
2832
|
+
smtpPort: settings.smtpPort,
|
|
2833
|
+
smtpSecure: settings.smtpSecure,
|
|
2834
|
+
smtpUser: settings.smtpUser,
|
|
2835
|
+
smtpPassword: settings.smtpPassword,
|
|
2756
2836
|
// Discount/promotion configuration
|
|
2757
2837
|
promotionCodesEnabled: settings.promotionCodesEnabled,
|
|
2758
2838
|
// Cookie consent banner (R021)
|
|
@@ -2842,6 +2922,15 @@ export async function upsertSettings(data) {
|
|
|
2842
2922
|
colissimoDefaultWeightKg: data.colissimoDefaultWeightKg !== null && data.colissimoDefaultWeightKg !== undefined
|
|
2843
2923
|
? String(data.colissimoDefaultWeightKg)
|
|
2844
2924
|
: undefined,
|
|
2925
|
+
// Email transport (SDK 0.23.0). Explicit null clears (disconnect).
|
|
2926
|
+
emailTransport: data.emailTransport !== undefined ? data.emailTransport : undefined,
|
|
2927
|
+
emailFromAddress: data.emailFromAddress !== undefined ? data.emailFromAddress : undefined,
|
|
2928
|
+
emailFromName: data.emailFromName !== undefined ? data.emailFromName : undefined,
|
|
2929
|
+
smtpHost: data.smtpHost !== undefined ? data.smtpHost : undefined,
|
|
2930
|
+
smtpPort: data.smtpPort !== undefined ? data.smtpPort : undefined,
|
|
2931
|
+
smtpSecure: data.smtpSecure !== undefined ? data.smtpSecure : undefined,
|
|
2932
|
+
smtpUser: data.smtpUser !== undefined ? data.smtpUser : undefined,
|
|
2933
|
+
smtpPassword: data.smtpPassword !== undefined ? data.smtpPassword : undefined,
|
|
2845
2934
|
// Discount/promotion configuration
|
|
2846
2935
|
promotionCodesEnabled: data.promotionCodesEnabled ?? undefined,
|
|
2847
2936
|
// Cookie consent banner (R021)
|
|
@@ -2901,6 +2990,15 @@ export async function upsertSettings(data) {
|
|
|
2901
2990
|
colissimoDefaultWeightKg: result.colissimoDefaultWeightKg
|
|
2902
2991
|
? Number(result.colissimoDefaultWeightKg)
|
|
2903
2992
|
: null,
|
|
2993
|
+
// Transactional-email transport (SDK 0.23.0)
|
|
2994
|
+
emailTransport: result.emailTransport ?? null,
|
|
2995
|
+
emailFromAddress: result.emailFromAddress,
|
|
2996
|
+
emailFromName: result.emailFromName,
|
|
2997
|
+
smtpHost: result.smtpHost,
|
|
2998
|
+
smtpPort: result.smtpPort,
|
|
2999
|
+
smtpSecure: result.smtpSecure,
|
|
3000
|
+
smtpUser: result.smtpUser,
|
|
3001
|
+
smtpPassword: result.smtpPassword,
|
|
2904
3002
|
// Discount/promotion configuration
|
|
2905
3003
|
promotionCodesEnabled: result.promotionCodesEnabled,
|
|
2906
3004
|
// Cookie consent banner (R021)
|
|
@@ -3401,6 +3499,25 @@ export async function findShippingOptionsForCountry(country, orderAmount, groupI
|
|
|
3401
3499
|
// Convert to array and sort by carrier name
|
|
3402
3500
|
return Array.from(carrierMap.values()).sort((a, b) => a.carrier.name.localeCompare(b.carrier.name));
|
|
3403
3501
|
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Sanitize a class-rates map at the write boundary: string keys (trimmed,
|
|
3504
|
+
* ≤50 chars — the products.tax_class width), finite rates clamped 0-100.
|
|
3505
|
+
* Invalid entries are dropped, never thrown — a bad row must not brick the
|
|
3506
|
+
* zone editor.
|
|
3507
|
+
*/
|
|
3508
|
+
export function sanitizeClassRates(input) {
|
|
3509
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
3510
|
+
return {};
|
|
3511
|
+
const out = {};
|
|
3512
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3513
|
+
const name = String(key).trim().slice(0, 50);
|
|
3514
|
+
const rate = typeof value === 'number' ? value : Number(value);
|
|
3515
|
+
if (!name || !Number.isFinite(rate) || rate < 0 || rate > 100)
|
|
3516
|
+
continue;
|
|
3517
|
+
out[name] = rate;
|
|
3518
|
+
}
|
|
3519
|
+
return out;
|
|
3520
|
+
}
|
|
3404
3521
|
/**
|
|
3405
3522
|
* Find all tax zones ordered by priority (sortOrder ascending)
|
|
3406
3523
|
*/
|
|
@@ -3418,6 +3535,7 @@ export async function findTaxZones() {
|
|
|
3418
3535
|
countries: zone.countries || [],
|
|
3419
3536
|
states: zone.states || [],
|
|
3420
3537
|
taxRate: Number(zone.taxRate),
|
|
3538
|
+
classRates: sanitizeClassRates(zone.classRates),
|
|
3421
3539
|
isActive: zone.isActive,
|
|
3422
3540
|
sortOrder: zone.sortOrder,
|
|
3423
3541
|
visibleToGroups: Array.isArray(zone.visibleToGroups) ? zone.visibleToGroups : [],
|
|
@@ -3452,6 +3570,7 @@ export async function createTaxZone(data) {
|
|
|
3452
3570
|
countries: data.countries.map((c) => c.toUpperCase()),
|
|
3453
3571
|
states: (data.states || []).map((s) => s.toUpperCase()),
|
|
3454
3572
|
taxRate: String(data.taxRate),
|
|
3573
|
+
classRates: sanitizeClassRates(data.classRates),
|
|
3455
3574
|
isActive: data.isActive ?? true,
|
|
3456
3575
|
sortOrder: data.sortOrder ?? 0,
|
|
3457
3576
|
visibleToGroups: data.visibleToGroups ?? [],
|
|
@@ -3474,6 +3593,8 @@ export async function updateTaxZone(id, data) {
|
|
|
3474
3593
|
updateData.states = data.states.map((s) => s.toUpperCase());
|
|
3475
3594
|
if (data.taxRate !== undefined)
|
|
3476
3595
|
updateData.taxRate = String(data.taxRate);
|
|
3596
|
+
if (data.classRates !== undefined)
|
|
3597
|
+
updateData.classRates = sanitizeClassRates(data.classRates);
|
|
3477
3598
|
if (data.isActive !== undefined)
|
|
3478
3599
|
updateData.isActive = data.isActive;
|
|
3479
3600
|
if (data.sortOrder !== undefined)
|
|
@@ -3537,6 +3658,7 @@ export async function findTaxRateForAddress(country, state, groupId) {
|
|
|
3537
3658
|
zoneId: zone.id,
|
|
3538
3659
|
zoneName: zone.name,
|
|
3539
3660
|
taxRate: zone.taxRate,
|
|
3661
|
+
classRates: zone.classRates,
|
|
3540
3662
|
};
|
|
3541
3663
|
}
|
|
3542
3664
|
return null; // No matching tax zone
|