@develemit/commerce-server 0.8.0 → 0.9.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/CHANGELOG.md +9 -0
- package/README.md +2 -1
- package/dist/index.d.ts +58 -4
- package/dist/index.js +70 -20
- package/dist/index.js.map +1 -1
- package/docs/payments.md +37 -0
- package/migrations/0010_order_tax.sql +1 -0
- package/migrations/0011_product_tax_code.sql +1 -0
- package/migrations/meta/0010_snapshot.json +1042 -0
- package/migrations/meta/0011_snapshot.json +1048 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.9.0 (2026-11-14)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- Products can carry an optional Stripe `taxCode`. Migration `0011` adds `commerce_products.tax_code`; the admin API reads and writes it, checkout sends it on that line (`CheckoutLine.taxCode`, which the Stripe gateway passes as `tax_code`), and the public catalog does not expose it.
|
|
6
|
+
- `orderEmailLines` adds a `tax` line (structured, text and HTML, label `labels.tax`, default "Tax") between shipping and total, only when the order's `taxCents` is above 0.
|
|
7
|
+
- Opt-in automatic sales tax through Stripe Tax: `createStripeGateway({ tax: { enabled: true, behavior?, shippingTaxCode? } })` (or `resolvePayment(env, { tax })`) sets `automatic_tax`, a `tax_behavior` on every line and shipping option, and a per-line `taxCode` (`CheckoutLine.taxCode`) when present. The completed-session webhook reads `total_details.amount_tax` into `taxCents`. Without the option, sessions are unchanged. The fake gateway accepts `tax: { rate }` and `taxFor()`, and `devCheckout` records the computed tax.
|
|
8
|
+
- Orders record sales tax. Migration `0010` adds `commerce_orders.tax_cents` (default 0). A `checkout.completed` `PaymentEvent` may carry `taxCents`; it is stored on the order and included in `totalCents`, so full refunds return the tax. Public and admin order responses return `taxCents`. The fake gateway's `completed()` accepts `taxCents`.
|
|
9
|
+
|
|
1
10
|
## 0.8.0 (2026-11-12)
|
|
2
11
|
|
|
3
12
|
### Features
|
package/README.md
CHANGED
|
@@ -136,7 +136,8 @@ onOrderPaid: async (order) => {
|
|
|
136
136
|
`formatOrderRef` in contracts); print it instead of the full id.
|
|
137
137
|
|
|
138
138
|
The result also has structured `items`, `subtotal`, `discount` (absent when
|
|
139
|
-
no discount applied), `shipping`, `total` and `address`
|
|
139
|
+
no discount applied), `shipping`, `tax` (absent when the order has no tax), `total` and `address`
|
|
140
|
+
(absent for pickup).
|
|
140
141
|
HTML output is `<tr>` / `<p>` fragments with `& < > " '` escaped. Override
|
|
141
142
|
copy with `labels`.
|
|
142
143
|
|
package/dist/index.d.ts
CHANGED
|
@@ -214,6 +214,23 @@ declare const commerceSchema: {
|
|
|
214
214
|
identity: undefined;
|
|
215
215
|
generated: undefined;
|
|
216
216
|
}, {}, {}>;
|
|
217
|
+
taxCode: drizzle_orm_pg_core.PgColumn<{
|
|
218
|
+
name: "tax_code";
|
|
219
|
+
tableName: "commerce_products";
|
|
220
|
+
dataType: "string";
|
|
221
|
+
columnType: "PgText";
|
|
222
|
+
data: string;
|
|
223
|
+
driverParam: string;
|
|
224
|
+
notNull: false;
|
|
225
|
+
hasDefault: false;
|
|
226
|
+
isPrimaryKey: false;
|
|
227
|
+
isAutoincrement: false;
|
|
228
|
+
hasRuntimeDefault: false;
|
|
229
|
+
enumValues: [string, ...string[]];
|
|
230
|
+
baseColumn: never;
|
|
231
|
+
identity: undefined;
|
|
232
|
+
generated: undefined;
|
|
233
|
+
}, {}, {}>;
|
|
217
234
|
publishedAt: drizzle_orm_pg_core.PgColumn<{
|
|
218
235
|
name: "published_at";
|
|
219
236
|
tableName: "commerce_products";
|
|
@@ -645,6 +662,23 @@ declare const commerceSchema: {
|
|
|
645
662
|
identity: undefined;
|
|
646
663
|
generated: undefined;
|
|
647
664
|
}, {}, {}>;
|
|
665
|
+
taxCents: drizzle_orm_pg_core.PgColumn<{
|
|
666
|
+
name: "tax_cents";
|
|
667
|
+
tableName: "commerce_orders";
|
|
668
|
+
dataType: "number";
|
|
669
|
+
columnType: "PgInteger";
|
|
670
|
+
data: number;
|
|
671
|
+
driverParam: string | number;
|
|
672
|
+
notNull: true;
|
|
673
|
+
hasDefault: true;
|
|
674
|
+
isPrimaryKey: false;
|
|
675
|
+
isAutoincrement: false;
|
|
676
|
+
hasRuntimeDefault: false;
|
|
677
|
+
enumValues: undefined;
|
|
678
|
+
baseColumn: never;
|
|
679
|
+
identity: undefined;
|
|
680
|
+
generated: undefined;
|
|
681
|
+
}, {}, {}>;
|
|
648
682
|
currency: drizzle_orm_pg_core.PgColumn<{
|
|
649
683
|
name: "currency";
|
|
650
684
|
tableName: "commerce_orders";
|
|
@@ -1686,6 +1720,7 @@ interface CheckoutLine {
|
|
|
1686
1720
|
name: string;
|
|
1687
1721
|
unitAmountCents: number;
|
|
1688
1722
|
quantity: number;
|
|
1723
|
+
taxCode?: string;
|
|
1689
1724
|
}
|
|
1690
1725
|
interface CreateCheckoutSessionInput {
|
|
1691
1726
|
reference: string;
|
|
@@ -1731,6 +1766,7 @@ type PaymentEvent = {
|
|
|
1731
1766
|
email?: string;
|
|
1732
1767
|
shippingAddress?: ShippingAddress;
|
|
1733
1768
|
shipping?: ChosenShipping;
|
|
1769
|
+
taxCents?: number;
|
|
1734
1770
|
paymentPending?: boolean;
|
|
1735
1771
|
} | {
|
|
1736
1772
|
id: string;
|
|
@@ -1844,8 +1880,14 @@ interface StripeGatewayOptions {
|
|
|
1844
1880
|
secretKey: string;
|
|
1845
1881
|
webhookSecret: string;
|
|
1846
1882
|
client?: Stripe;
|
|
1883
|
+
tax?: StripeTaxOptions;
|
|
1884
|
+
}
|
|
1885
|
+
interface StripeTaxOptions {
|
|
1886
|
+
enabled: true;
|
|
1887
|
+
behavior?: 'exclusive' | 'inclusive';
|
|
1888
|
+
shippingTaxCode?: string;
|
|
1847
1889
|
}
|
|
1848
|
-
declare function createStripeGateway({ secretKey, webhookSecret, client, }: StripeGatewayOptions): PaymentGateway;
|
|
1890
|
+
declare function createStripeGateway({ secretKey, webhookSecret, client, tax, }: StripeGatewayOptions): PaymentGateway;
|
|
1849
1891
|
|
|
1850
1892
|
interface SignedWebhook {
|
|
1851
1893
|
rawBody: Buffer;
|
|
@@ -1868,6 +1910,7 @@ interface FakeGateway extends PaymentGateway {
|
|
|
1868
1910
|
email?: string;
|
|
1869
1911
|
shippingAddress?: ShippingAddress;
|
|
1870
1912
|
shipping?: ChosenShipping;
|
|
1913
|
+
taxCents?: number;
|
|
1871
1914
|
paymentPending?: boolean;
|
|
1872
1915
|
}): SignedWebhook;
|
|
1873
1916
|
asyncPaid(sessionId: string): SignedWebhook;
|
|
@@ -1875,11 +1918,16 @@ interface FakeGateway extends PaymentGateway {
|
|
|
1875
1918
|
expired(sessionId: string): SignedWebhook;
|
|
1876
1919
|
refunded(sessionId: string, refunds: GatewayRefundEvent[]): SignedWebhook;
|
|
1877
1920
|
sign(event: PaymentEvent): SignedWebhook;
|
|
1921
|
+
taxFor(sessionId: string, shippingCents?: number): number;
|
|
1922
|
+
}
|
|
1923
|
+
interface FakeTaxOptions {
|
|
1924
|
+
rate: number;
|
|
1878
1925
|
}
|
|
1879
1926
|
interface FakeGatewayOptions {
|
|
1880
1927
|
secret?: string;
|
|
1928
|
+
tax?: FakeTaxOptions;
|
|
1881
1929
|
}
|
|
1882
|
-
declare function createFakeGateway({ secret, }?: FakeGatewayOptions): FakeGateway;
|
|
1930
|
+
declare function createFakeGateway({ secret, tax, }?: FakeGatewayOptions): FakeGateway;
|
|
1883
1931
|
declare function isFakeGateway(gateway: PaymentGateway): gateway is FakeGateway;
|
|
1884
1932
|
|
|
1885
1933
|
interface HoldSweeperOptions {
|
|
@@ -1899,6 +1947,7 @@ interface OrderEmailLabels {
|
|
|
1899
1947
|
subtotal: string;
|
|
1900
1948
|
discount: (code: string | null) => string;
|
|
1901
1949
|
shipping: (label: string | null) => string;
|
|
1950
|
+
tax: string;
|
|
1902
1951
|
total: string;
|
|
1903
1952
|
}
|
|
1904
1953
|
interface OrderEmailLinesOptions {
|
|
@@ -1921,6 +1970,7 @@ interface OrderEmailLines {
|
|
|
1921
1970
|
subtotal: EmailLine;
|
|
1922
1971
|
discount?: EmailLine;
|
|
1923
1972
|
shipping: EmailLine;
|
|
1973
|
+
tax?: EmailLine;
|
|
1924
1974
|
total: EmailLine;
|
|
1925
1975
|
address?: string[];
|
|
1926
1976
|
text: OrderEmailLinesText;
|
|
@@ -1986,7 +2036,11 @@ interface PaymentEnv {
|
|
|
1986
2036
|
STRIPE_SECRET_KEY?: string;
|
|
1987
2037
|
STRIPE_WEBHOOK_SECRET?: string;
|
|
1988
2038
|
}
|
|
1989
|
-
|
|
2039
|
+
interface ResolvePaymentOptions {
|
|
2040
|
+
tax?: StripeTaxOptions;
|
|
2041
|
+
fakeTax?: FakeTaxOptions;
|
|
2042
|
+
}
|
|
2043
|
+
declare function resolvePayment(env?: PaymentEnv, { tax, fakeTax }?: ResolvePaymentOptions): PaymentGateway;
|
|
1990
2044
|
interface CheckoutUrlPaths {
|
|
1991
2045
|
successPath?: string;
|
|
1992
2046
|
cancelPath?: string;
|
|
@@ -1994,4 +2048,4 @@ interface CheckoutUrlPaths {
|
|
|
1994
2048
|
declare function checkoutUrls(webUrl: string, { successPath, cancelPath, }?: CheckoutUrlPaths): CheckoutUrls;
|
|
1995
2049
|
|
|
1996
2050
|
export { ProductNotFoundError, SESSION_ID_PLACEHOLDER, WebhookSignatureError, catalogPlugin, checkoutUrls, commercePlugin, commerceSchema, createDiskStorage, createFakeGateway, createMemoryStorage, createStripeGateway, isFakeGateway, openStoredFile, orderEmailLines, ordersPlugin, resolvePayment, runCommerceMigrations, seedProducts, seedShippingRates, startHoldSweeper };
|
|
1997
|
-
export type { AdminOptions, CatalogCacheOptions, CatalogPluginOptions, CategoryLabels, CheckoutDiscount, CheckoutLine, CheckoutSession, CheckoutUrlPaths, CheckoutUrls, ChosenShipping, CommerceDb, CommercePluginOptions, CommerceSchema, CreateCheckoutSessionInput, DevBuyer, DevBuyerSetting, DevCheckoutSetting, DiskStorageOptions, EmailLine, FakeGateway, FakeGatewayOptions, FileStorage, GatewayRefund, HoldSweeper, HoldSweeperOptions, MemoryStorage, OrderEmailLabels, OrderEmailLines, OrderEmailLinesOptions, OrderHooks, OrdersPluginOptions, PaymentEnv, PaymentEvent, PaymentGateway, RefundInput, SeedProduct, SeedShippingRate, ShippingAddress, ShippingRateOption, SignedWebhook, StoredFile, StoredFileStream, StripeGatewayOptions };
|
|
2051
|
+
export type { AdminOptions, CatalogCacheOptions, CatalogPluginOptions, CategoryLabels, CheckoutDiscount, CheckoutLine, CheckoutSession, CheckoutUrlPaths, CheckoutUrls, ChosenShipping, CommerceDb, CommercePluginOptions, CommerceSchema, CreateCheckoutSessionInput, DevBuyer, DevBuyerSetting, DevCheckoutSetting, DiskStorageOptions, EmailLine, FakeGateway, FakeGatewayOptions, FakeTaxOptions, FileStorage, GatewayRefund, HoldSweeper, HoldSweeperOptions, MemoryStorage, OrderEmailLabels, OrderEmailLines, OrderEmailLinesOptions, OrderHooks, OrdersPluginOptions, PaymentEnv, PaymentEvent, PaymentGateway, RefundInput, ResolvePaymentOptions, SeedProduct, SeedShippingRate, ShippingAddress, ShippingRateOption, SignedWebhook, StoredFile, StoredFileStream, StripeGatewayOptions, StripeTaxOptions };
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,7 @@ var products = pgTable(
|
|
|
82
82
|
quantity: integer("quantity").notNull().default(1),
|
|
83
83
|
status: productStatus("status").notNull().default("draft"),
|
|
84
84
|
featured: boolean("featured").notNull().default(false),
|
|
85
|
+
taxCode: text("tax_code"),
|
|
85
86
|
publishedAt: timestamp("published_at", { withTimezone: true }),
|
|
86
87
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
87
88
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
@@ -139,6 +140,7 @@ var orders = pgTable("commerce_orders", {
|
|
|
139
140
|
discountCode: text("discount_code"),
|
|
140
141
|
shippingCents: integer("shipping_cents").notNull().default(0),
|
|
141
142
|
shippingLabel: text("shipping_label"),
|
|
143
|
+
taxCents: integer("tax_cents").notNull().default(0),
|
|
142
144
|
currency: text("currency").notNull(),
|
|
143
145
|
checkoutSessionId: text("checkout_session_id").unique(),
|
|
144
146
|
shippingAddress: jsonb("shipping_address"),
|
|
@@ -548,6 +550,7 @@ function toAdminProduct({ row, images }) {
|
|
|
548
550
|
status: row.status,
|
|
549
551
|
featured: row.featured,
|
|
550
552
|
everPublished: row.publishedAt !== null,
|
|
553
|
+
taxCode: row.taxCode,
|
|
551
554
|
images: images.map((i) => ({
|
|
552
555
|
id: i.id,
|
|
553
556
|
url: i.url,
|
|
@@ -1226,7 +1229,8 @@ async function findPurchasable(db, ids) {
|
|
|
1226
1229
|
id: row.id,
|
|
1227
1230
|
name: row.name,
|
|
1228
1231
|
priceCents: completeFields(row).priceCents,
|
|
1229
|
-
currency: row.currency
|
|
1232
|
+
currency: row.currency,
|
|
1233
|
+
taxCode: row.taxCode
|
|
1230
1234
|
}));
|
|
1231
1235
|
}
|
|
1232
1236
|
|
|
@@ -1318,7 +1322,8 @@ async function selectOrderPage(db, filter) {
|
|
|
1318
1322
|
id: orders.id,
|
|
1319
1323
|
status: orders.status,
|
|
1320
1324
|
email: orders.email,
|
|
1321
|
-
|
|
1325
|
+
taxCents: orders.taxCents,
|
|
1326
|
+
totalCents: sql5`${orders.subtotalCents} - ${orders.discountCents} + ${orders.shippingCents} + ${orders.taxCents}`,
|
|
1322
1327
|
currency: orders.currency,
|
|
1323
1328
|
itemCount,
|
|
1324
1329
|
createdAt: orders.createdAt,
|
|
@@ -1341,7 +1346,7 @@ var isRecordedRefund = or4(
|
|
|
1341
1346
|
async function lockOrderForRefund(tx, id) {
|
|
1342
1347
|
const [row] = await tx.select({
|
|
1343
1348
|
status: orders.status,
|
|
1344
|
-
totalCents: sql6`${orders.subtotalCents} - ${orders.discountCents} + ${orders.shippingCents}`,
|
|
1349
|
+
totalCents: sql6`${orders.subtotalCents} - ${orders.discountCents} + ${orders.shippingCents} + ${orders.taxCents}`,
|
|
1345
1350
|
currency: orders.currency,
|
|
1346
1351
|
checkoutSessionId: orders.checkoutSessionId
|
|
1347
1352
|
}).from(orders).where(eq6(orders.id, id)).for("update");
|
|
@@ -1433,6 +1438,7 @@ async function saveOrderDetails(db, orderId, details, status) {
|
|
|
1433
1438
|
shippingAddress: details.shippingAddress ?? null,
|
|
1434
1439
|
shippingCents: details.shipping?.amountCents ?? 0,
|
|
1435
1440
|
shippingLabel: details.shipping?.label ?? null,
|
|
1441
|
+
taxCents: details.taxCents ?? 0,
|
|
1436
1442
|
updatedAt: sql7`now()`
|
|
1437
1443
|
}).where(eq7(orders.id, orderId));
|
|
1438
1444
|
}
|
|
@@ -1478,7 +1484,8 @@ async function toSummary(db, order) {
|
|
|
1478
1484
|
discountCode: order.discountCode,
|
|
1479
1485
|
shippingCents: order.shippingCents,
|
|
1480
1486
|
shippingLabel: order.shippingLabel,
|
|
1481
|
-
|
|
1487
|
+
taxCents: order.taxCents,
|
|
1488
|
+
totalCents: order.subtotalCents - order.discountCents + order.shippingCents + order.taxCents,
|
|
1482
1489
|
currency: order.currency,
|
|
1483
1490
|
items: items.map((i) => ({
|
|
1484
1491
|
productId: i.productId,
|
|
@@ -1558,6 +1565,7 @@ async function listAdminOrders(db, query) {
|
|
|
1558
1565
|
id: r.id,
|
|
1559
1566
|
status: r.status,
|
|
1560
1567
|
email: r.email,
|
|
1568
|
+
taxCents: r.taxCents,
|
|
1561
1569
|
totalCents: r.totalCents,
|
|
1562
1570
|
currency: r.currency,
|
|
1563
1571
|
itemCount: r.itemCount,
|
|
@@ -2536,6 +2544,15 @@ function assertDevCheckoutAllowed() {
|
|
|
2536
2544
|
throw new Error("devCheckout is not allowed when NODE_ENV=production");
|
|
2537
2545
|
}
|
|
2538
2546
|
}
|
|
2547
|
+
function completedWithTax(payment, session, buyer) {
|
|
2548
|
+
const details = resolveDevCompletion(
|
|
2549
|
+
buyer,
|
|
2550
|
+
session.id,
|
|
2551
|
+
session.shippingRates[0]
|
|
2552
|
+
);
|
|
2553
|
+
const taxCents = payment.taxFor(session.id, details.shipping?.amountCents);
|
|
2554
|
+
return payment.completed(session.id, { ...details, taxCents });
|
|
2555
|
+
}
|
|
2539
2556
|
var devCheckoutRoutes = async (app, { db, payment, urls, hooks, buyer }) => {
|
|
2540
2557
|
assertDevCheckoutAllowed();
|
|
2541
2558
|
app.get("/checkout/dev/:sessionId/complete", async (req, reply) => {
|
|
@@ -2545,10 +2562,7 @@ var devCheckoutRoutes = async (app, { db, payment, urls, hooks, buyer }) => {
|
|
|
2545
2562
|
return reply.code(404).send({ error: "SESSION_NOT_FOUND" });
|
|
2546
2563
|
}
|
|
2547
2564
|
const expire = req.query.outcome === "expired";
|
|
2548
|
-
const { rawBody, signature } = expire ? payment.expired(sessionId) : payment
|
|
2549
|
-
sessionId,
|
|
2550
|
-
resolveDevCompletion(buyer, sessionId, session.shippingRates[0])
|
|
2551
|
-
);
|
|
2565
|
+
const { rawBody, signature } = expire ? payment.expired(sessionId) : completedWithTax(payment, session, buyer);
|
|
2552
2566
|
const event = await payment.verifyWebhook(rawBody, signature);
|
|
2553
2567
|
await handlePaymentEvent(db, event, hooks, req.log);
|
|
2554
2568
|
return reply.redirect(expire ? urls.cancel : urls.success(sessionId), 302);
|
|
@@ -2632,7 +2646,8 @@ async function priceLines(db, requests, currency) {
|
|
|
2632
2646
|
productId,
|
|
2633
2647
|
name: product.name,
|
|
2634
2648
|
unitAmountCents: product.priceCents,
|
|
2635
|
-
quantity
|
|
2649
|
+
quantity,
|
|
2650
|
+
...product.taxCode && { taxCode: product.taxCode }
|
|
2636
2651
|
};
|
|
2637
2652
|
});
|
|
2638
2653
|
}
|
|
@@ -2822,7 +2837,8 @@ var ordersPlugin = async (app, { db, ...settings }) => {
|
|
|
2822
2837
|
// src/payment/fake-gateway.ts
|
|
2823
2838
|
import { createHmac, randomUUID as randomUUID4, timingSafeEqual } from "crypto";
|
|
2824
2839
|
function createFakeGateway({
|
|
2825
|
-
secret = "fake-webhook-secret"
|
|
2840
|
+
secret = "fake-webhook-secret",
|
|
2841
|
+
tax
|
|
2826
2842
|
} = {}) {
|
|
2827
2843
|
const sessions = [];
|
|
2828
2844
|
const refunds = [];
|
|
@@ -2925,7 +2941,17 @@ function createFakeGateway({
|
|
|
2925
2941
|
refunds: refunds2
|
|
2926
2942
|
});
|
|
2927
2943
|
},
|
|
2928
|
-
sign
|
|
2944
|
+
sign,
|
|
2945
|
+
taxFor(sessionId, shippingCents = 0) {
|
|
2946
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
2947
|
+
if (!tax || !session) return 0;
|
|
2948
|
+
const merchandise = session.lines.reduce(
|
|
2949
|
+
(sum, l) => sum + l.unitAmountCents * l.quantity,
|
|
2950
|
+
0
|
|
2951
|
+
);
|
|
2952
|
+
const taxable = merchandise - (session.discount?.amountCents ?? 0) + shippingCents;
|
|
2953
|
+
return Math.round(taxable * tax.rate);
|
|
2954
|
+
}
|
|
2929
2955
|
};
|
|
2930
2956
|
}
|
|
2931
2957
|
function isFakeGateway(gateway) {
|
|
@@ -3041,13 +3067,17 @@ function toShippingAddress(name, address) {
|
|
|
3041
3067
|
Object.entries(fields).filter(([, v]) => v !== void 0)
|
|
3042
3068
|
);
|
|
3043
3069
|
}
|
|
3044
|
-
function toShippingOption(rate, currency) {
|
|
3070
|
+
function toShippingOption(rate, currency, tax) {
|
|
3045
3071
|
const days = (value) => ({ unit: "business_day", value });
|
|
3046
3072
|
return {
|
|
3047
3073
|
shipping_rate_data: {
|
|
3048
3074
|
type: "fixed_amount",
|
|
3049
3075
|
display_name: rate.label,
|
|
3050
3076
|
fixed_amount: { amount: rate.amountCents, currency },
|
|
3077
|
+
...tax && {
|
|
3078
|
+
tax_behavior: tax.behavior ?? "exclusive",
|
|
3079
|
+
...tax.shippingTaxCode && { tax_code: tax.shippingTaxCode }
|
|
3080
|
+
},
|
|
3051
3081
|
...rate.minDays !== void 0 && rate.maxDays !== void 0 && {
|
|
3052
3082
|
delivery_estimate: {
|
|
3053
3083
|
minimum: days(rate.minDays),
|
|
@@ -3063,7 +3093,7 @@ function toChosenShipping(session) {
|
|
|
3063
3093
|
if (!cost || !rate || typeof rate === "string") return void 0;
|
|
3064
3094
|
return {
|
|
3065
3095
|
label: rate.display_name ?? "Shipping",
|
|
3066
|
-
amountCents: cost.
|
|
3096
|
+
amountCents: cost.amount_subtotal
|
|
3067
3097
|
};
|
|
3068
3098
|
}
|
|
3069
3099
|
function toPaymentEvent(event, expanded) {
|
|
@@ -3077,6 +3107,9 @@ function toPaymentEvent(event, expanded) {
|
|
|
3077
3107
|
email: session.customer_details?.email ?? void 0,
|
|
3078
3108
|
shippingAddress: toShippingAddress(shipping?.name, shipping?.address),
|
|
3079
3109
|
shipping: expanded && toChosenShipping(expanded),
|
|
3110
|
+
...session.total_details && {
|
|
3111
|
+
taxCents: session.total_details.amount_tax
|
|
3112
|
+
},
|
|
3080
3113
|
paymentPending: session.payment_status === "unpaid"
|
|
3081
3114
|
};
|
|
3082
3115
|
}
|
|
@@ -3145,7 +3178,8 @@ function createCoupon(stripe, { discount, currency, expiresAt }) {
|
|
|
3145
3178
|
function createStripeGateway({
|
|
3146
3179
|
secretKey,
|
|
3147
3180
|
webhookSecret,
|
|
3148
|
-
client
|
|
3181
|
+
client,
|
|
3182
|
+
tax
|
|
3149
3183
|
}) {
|
|
3150
3184
|
let stripe = client ? Promise.resolve(client) : void 0;
|
|
3151
3185
|
const getStripe = () => stripe ??= loadStripe(secretKey);
|
|
@@ -3165,19 +3199,22 @@ function createStripeGateway({
|
|
|
3165
3199
|
price_data: {
|
|
3166
3200
|
currency: input.currency.toLowerCase(),
|
|
3167
3201
|
unit_amount: line.unitAmountCents,
|
|
3202
|
+
...tax && { tax_behavior: tax.behavior ?? "exclusive" },
|
|
3168
3203
|
product_data: {
|
|
3169
3204
|
name: line.name,
|
|
3170
|
-
metadata: { productId: line.productId }
|
|
3205
|
+
metadata: { productId: line.productId },
|
|
3206
|
+
...tax && line.taxCode && { tax_code: line.taxCode }
|
|
3171
3207
|
}
|
|
3172
3208
|
}
|
|
3173
3209
|
})),
|
|
3210
|
+
...tax && { automatic_tax: { enabled: true } },
|
|
3174
3211
|
...coupon && { discounts: [{ coupon: coupon.id }] },
|
|
3175
3212
|
success_url: input.successUrl,
|
|
3176
3213
|
cancel_url: input.cancelUrl,
|
|
3177
3214
|
expires_at: Math.floor(input.expiresAt.getTime() / 1e3),
|
|
3178
3215
|
...input.shippingRates.length > 0 && {
|
|
3179
3216
|
shipping_options: input.shippingRates.map(
|
|
3180
|
-
(rate) => toShippingOption(rate, input.currency.toLowerCase())
|
|
3217
|
+
(rate) => toShippingOption(rate, input.currency.toLowerCase(), tax)
|
|
3181
3218
|
)
|
|
3182
3219
|
},
|
|
3183
3220
|
shipping_address_collection: {
|
|
@@ -3257,6 +3294,7 @@ var defaultLabels = {
|
|
|
3257
3294
|
subtotal: "Subtotal",
|
|
3258
3295
|
discount: (code) => code ? `Discount (${code})` : "Discount",
|
|
3259
3296
|
shipping: (label) => label ? `Shipping (${label})` : "Shipping",
|
|
3297
|
+
tax: "Tax",
|
|
3260
3298
|
total: "Total"
|
|
3261
3299
|
};
|
|
3262
3300
|
var escapes = {
|
|
@@ -3312,15 +3350,23 @@ function orderEmailLines(order, options = {}) {
|
|
|
3312
3350
|
label: labels.shipping(order.shippingLabel),
|
|
3313
3351
|
amount: money(order.shippingCents)
|
|
3314
3352
|
};
|
|
3353
|
+
const tax = order.taxCents > 0 ? { label: labels.tax, amount: money(order.taxCents) } : void 0;
|
|
3315
3354
|
const total = { label: labels.total, amount: money(order.totalCents) };
|
|
3316
3355
|
const address = addressLines(order);
|
|
3317
|
-
const totals = [
|
|
3356
|
+
const totals = [
|
|
3357
|
+
subtotal,
|
|
3358
|
+
...discount ? [discount] : [],
|
|
3359
|
+
shipping,
|
|
3360
|
+
...tax ? [tax] : [],
|
|
3361
|
+
total
|
|
3362
|
+
];
|
|
3318
3363
|
return {
|
|
3319
3364
|
reference: formatOrderRef(order.id),
|
|
3320
3365
|
items,
|
|
3321
3366
|
subtotal,
|
|
3322
3367
|
discount,
|
|
3323
3368
|
shipping,
|
|
3369
|
+
tax,
|
|
3324
3370
|
total,
|
|
3325
3371
|
address,
|
|
3326
3372
|
text: {
|
|
@@ -3492,7 +3538,7 @@ async function openStoredFile(dir, key) {
|
|
|
3492
3538
|
}
|
|
3493
3539
|
|
|
3494
3540
|
// src/payment/resolve.ts
|
|
3495
|
-
function resolvePayment(env = process.env) {
|
|
3541
|
+
function resolvePayment(env = process.env, { tax, fakeTax } = {}) {
|
|
3496
3542
|
const secretKey = env.STRIPE_SECRET_KEY || void 0;
|
|
3497
3543
|
const webhookSecret = env.STRIPE_WEBHOOK_SECRET || void 0;
|
|
3498
3544
|
if (!secretKey !== !webhookSecret) {
|
|
@@ -3509,9 +3555,13 @@ function resolvePayment(env = process.env) {
|
|
|
3509
3555
|
"The fake payment gateway is not allowed in production; set the Stripe keys"
|
|
3510
3556
|
);
|
|
3511
3557
|
}
|
|
3512
|
-
return createFakeGateway();
|
|
3558
|
+
return createFakeGateway({ tax: fakeTax });
|
|
3513
3559
|
}
|
|
3514
|
-
return createStripeGateway({
|
|
3560
|
+
return createStripeGateway({
|
|
3561
|
+
secretKey,
|
|
3562
|
+
webhookSecret,
|
|
3563
|
+
tax
|
|
3564
|
+
});
|
|
3515
3565
|
}
|
|
3516
3566
|
function checkoutUrls(webUrl, {
|
|
3517
3567
|
successPath = "/checkout/success",
|