@saastemly/voidcommerce 0.1.0 → 0.2.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/catalog.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  SIGN_IN,
15
15
  TAX,
16
16
  UI
17
- } from "./index-s7sq41qs.js";
17
+ } from "./index-yk361vn2.js";
18
18
  export {
19
19
  UI,
20
20
  TAX,
package/dist/cli.js CHANGED
@@ -22,16 +22,16 @@ import {
22
22
  runVoid,
23
23
  version,
24
24
  voidAppsIn
25
- } from "./index-wzy1xtr1.js";
25
+ } from "./index-9x9q9ng2.js";
26
26
  import {
27
27
  LAYOUTS,
28
28
  readManifest,
29
29
  validate,
30
30
  zoneOf
31
- } from "./index-ssv3a6wc.js";
31
+ } from "./index-0vt6g4cb.js";
32
32
  import {
33
33
  GROUPS
34
- } from "./index-s7sq41qs.js";
34
+ } from "./index-yk361vn2.js";
35
35
 
36
36
  // src/deploy/index.ts
37
37
  import color from "picocolors";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CHOICES,
3
3
  GROUPS
4
- } from "./index-s7sq41qs.js";
4
+ } from "./index-yk361vn2.js";
5
5
 
6
6
  // src/manifest.ts
7
7
  import { readFile, writeFile } from "node:fs/promises";
@@ -11,11 +11,11 @@ import {
11
11
  workerHosts,
12
12
  writeManifest,
13
13
  zone
14
- } from "./index-ssv3a6wc.js";
14
+ } from "./index-0vt6g4cb.js";
15
15
  import {
16
16
  CHOICES,
17
17
  __require
18
- } from "./index-s7sq41qs.js";
18
+ } from "./index-yk361vn2.js";
19
19
 
20
20
  // src/generate/auth.ts
21
21
  var VAT = {
@@ -38,9 +38,29 @@ var social = (manifest, id, envPrefix) => has(manifest, id) ? ` ${id}: { clien
38
38
  function renderAuthTs(manifest) {
39
39
  const { shop } = manifest;
40
40
  const lines = [];
41
+ const CARRIER_CALLS = {
42
+ "dhl-express": {
43
+ label: "DHL Express",
44
+ call: `dhlExpressProvider({ apiKey: must(env, "DHL_API_KEY") })`,
45
+ import: `import { dhlExpressProvider } from "@saastemly/better-commerce/providers/dhl-express";`
46
+ },
47
+ shippo: { label: "Shippo", call: `shippoProvider({ apiKey: must(env, "SHIPPO_API_KEY") })`, import: `import { shippoProvider } from "@saastemly/better-commerce/providers/shippo";` },
48
+ easypost: { label: "EasyPost", call: `easypostProvider({ apiKey: must(env, "EASYPOST_API_KEY") })`, import: `import { easypostProvider } from "@saastemly/better-commerce/providers/easypost";` },
49
+ ups: { label: "UPS", call: `upsProvider({ clientId: must(env, "UPS_CLIENT_ID"), clientSecret: must(env, "UPS_CLIENT_SECRET") })`, import: `import { upsProvider } from "@saastemly/better-commerce/providers/ups";` },
50
+ fedex: { label: "FedEx", call: `fedexProvider({ clientId: must(env, "FEDEX_CLIENT_ID"), clientSecret: must(env, "FEDEX_CLIENT_SECRET") })`, import: `import { fedexProvider } from "@saastemly/better-commerce/providers/fedex";` },
51
+ "royal-mail": { label: "Royal Mail", call: `royalMailProvider({ clientId: must(env, "ROYAL_MAIL_CLIENT_ID") })`, import: `import { royalMailProvider } from "@saastemly/better-commerce/providers/royal-mail";` },
52
+ nft: {
53
+ label: "A token, not a parcel",
54
+ call: `nftDelivery({ mint: mintProvider, destinationFor })`,
55
+ import: `import { nftDelivery } from "@saastemly/better-commerce/providers/nft-delivery";
56
+ import { mintProvider, destinationFor } from "./lib/mint.ts";`
57
+ }
58
+ };
59
+ const carriers = Object.keys(CARRIER_CALLS).filter((id) => has(manifest, id));
60
+ const carrierImports = carriers.map((id) => CARRIER_CALLS[id].import);
41
61
  const wantsNft = has(manifest, "nft");
42
- const nftProvider = wantsNft ? `, nftOrPhysical({ mint: mintProvider, physical: ${has(manifest, "business-central") ? `erpFulfillment("${shop.currency.toUpperCase()}")` : "manualFulfillment()"} })` : "";
43
- const nftDefault = wantsNft ? `, defaultProvider: "nft-or-physical"` : "";
62
+ const carrierFulfillment = carriers.length ? `, deliveryFulfillment([${carriers.map((id) => CARRIER_CALLS[id].call).join(", ")}])` : "";
63
+ const nftDefault = carriers.length ? `, defaultProvider: "delivery:${carriers.join("+")}"` : "";
44
64
  const vat = VAT[shop.country];
45
65
  lines.push({
46
66
  import: `import { admin } from "better-auth/plugins/admin";`,
@@ -168,9 +188,12 @@ import { memorySearchProvider } from "@saastemly/better-commerce/providers/searc
168
188
  commerce("reviews", `reviews()`, "verified badge derived from a paid order");
169
189
  commerce("customer-groups", `customerGroups()`, "segments with their own prices");
170
190
  commerce("acp", `acp({ merchantDisplayName: "${shop.name}" })`, "agentic commerce", `import { acp } from "@saastemly/better-commerce/acp";`);
171
- commerce("fulfillments", `fulfillments({ providers: [authFulfillment(), creditsFulfillment()${has(manifest, "gift-cards") ? ", giftCardFulfillment()" : ""}${has(manifest, "business-central") ? `, erpFulfillment("${shop.currency.toUpperCase()}")` : ""}${nftProvider}]${nftDefault} })`, "what happens when the money is confirmed", `import { authFulfillment, creditsFulfillment, fulfillments, manualFulfillment } from "@saastemly/better-commerce/plugins/fulfillments";${has(manifest, "business-central") ? `
191
+ commerce("fulfillments", `fulfillments({ providers: [authFulfillment(), creditsFulfillment()${has(manifest, "gift-cards") ? ", giftCardFulfillment()" : ""}${has(manifest, "business-central") ? `, erpFulfillment("${shop.currency.toUpperCase()}")` : ""}${carrierFulfillment}]${nftDefault} })`, "what happens when the money is confirmed", `import { authFulfillment, creditsFulfillment, fulfillments, manualFulfillment } from "@saastemly/better-commerce/plugins/fulfillments";${carriers.length ? `
192
+ import { deliveryFulfillment } from "@saastemly/better-commerce/providers/delivery";` : ""}${carrierImports.length ? `
193
+ ${carrierImports.join(`
194
+ `)}` : ""}${has(manifest, "business-central") ? `
172
195
  import { erpFulfillment } from "./lib/erp.ts";` : ""}`);
173
- commerce("shipping-zones", `shippingZones({ zones: [{ id: "domestic", name: "${shop.country}", countries: ["${shop.country}"], rank: 0 }], shippingOptions: [{ id: "standard", zoneId: "domestic", name: "Standard", priceType: "flat", amount: Number(must(env, "SHIPPING_DOMESTIC_MINOR")), currency: "${shop.currency}", rank: 0 }, { id: "free", zoneId: "domestic", name: "Free", priceType: "free", currency: "${shop.currency}", minSubtotal: Number(must(env, "SHIPPING_FREE_FROM_MINOR")), rank: 1 }] })`, "quoted from required env; a shop with no option quotes FREE delivery silently");
196
+ commerce("shipping-zones", `shippingZones({ zones: [{ id: "domestic", name: "${shop.country}", countries: ["${shop.country}"], rank: 0 }], shippingOptions: [${carriers.length === 0 ? `{ id: "standard", zoneId: "domestic", name: "Standard", priceType: "flat", amount: Number(must(env, "SHIPPING_DOMESTIC_MINOR")), currency: "${shop.currency}", rank: 0 }, { id: "free", zoneId: "domestic", name: "Free", priceType: "free", currency: "${shop.currency}", minSubtotal: Number(must(env, "SHIPPING_FREE_FROM_MINOR")), rank: 1 }` : carriers.map((id, rank) => `{ id: "${id}", zoneId: "domestic", name: "${CARRIER_CALLS[id].label}", priceType: "calculated", carrierProviderId: "${id === "nft" ? "nft" : id}", currency: "${shop.currency}", rank: ${rank} }`).join(", ")}]${carriers.length ? `, carriers: [${carriers.map((id) => CARRIER_CALLS[id].call).join(", ")}]` : ""} })`, "quoted from required env; a shop with no option quotes FREE delivery silently");
174
197
  const registered = shop.taxRegistered === true;
175
198
  const askedLive = has(manifest, "stripe-tax");
176
199
  commerce("tax-regions", askedLive ? `taxRegions({ regions: [{ countryCode: "${shop.country}", name: "${shop.country}", rates: [] }], attachTax: false })` : !registered ? `taxRegions({ regions: [{ countryCode: "${shop.country}", name: "${shop.country}", rates: [] }] })` : vat ? `taxRegions({ regions: [{ countryCode: "${shop.country}", name: "${shop.country}", rates: [{ name: "${vat.name}", code: "vat", rate: ${vat.rate} }] }] })` : `taxRegions({ regions: [{ countryCode: "${shop.country}", name: "${shop.country}", rates: [] }] /* TODO VERIFY: no standard rate known for ${shop.country} */ })`, askedLive ? "Stripe is asked per basket, so an unregistered shop charges nothing and a registered one charges the buyer's own rate — with nothing here to keep up to date" : !registered ? `NOT registered for ${vat?.name ?? "VAT"}: no tax is added, because a business below the threshold must not charge it. Set taxRegistered in voidcommerce.json when you register.` : vat ? `${vat.rate / 100}% ${vat.name} declared in code, so a fresh deploy is never tax-free` : "TODO VERIFY: registered, but no standard rate is known for this country — declare it");
@@ -191,8 +214,7 @@ import { memoryFileProvider } from "@saastemly/better-commerce/providers/file";`
191
214
  commerce("merchants", `merchants()`, "several sellers, with payouts");
192
215
  if (has(manifest, "nft"))
193
216
  lines.push({
194
- import: `import { nft, nftOrPhysical } from "@saastemly/better-commerce/plugins/nft";
195
- import { mintProvider } from "./lib/mint.ts";`,
217
+ import: `import { nft } from "@saastemly/better-commerce/plugins/nft";`,
196
218
  call: `nft({ blurb: "Rather not have the object? Take an authentic, completely useless token of it instead. Nothing gets printed, nothing gets posted." })`,
197
219
  ring: "commerce",
198
220
  why: "the buyer chooses a parcel or a token; the catalogue is the same either way"
@@ -1397,7 +1419,7 @@ import color from "picocolors";
1397
1419
  // package.json
1398
1420
  var package_default = {
1399
1421
  name: "@saastemly/voidcommerce",
1400
- version: "0.1.0",
1422
+ version: "0.2.0",
1401
1423
  description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
1402
1424
  type: "module",
1403
1425
  license: "MIT",
@@ -2138,6 +2160,41 @@ function renderMintTs() {
2138
2160
  * after payment, and an exception there strands a paid order.
2139
2161
  */
2140
2162
  export const mintProvider: MintProvider = recordedMint();
2163
+
2164
+ /**
2165
+ * Where THIS order's token goes.
2166
+ *
2167
+ * The buyer's own email, because a custodial mint provider will make them a
2168
+ * wallet from it — so somebody who has never held one still receives the
2169
+ * token, and the checkout never has to ask for a wallet address. Returning
2170
+ * null refuses the shipment rather than minting into the void.
2171
+ *
2172
+ * The adapter is reached the way the emailer reaches it: through the current
2173
+ * auth context, which exists because this runs inside a request.
2174
+ */
2175
+ export async function destinationFor(orderId: string): Promise<string | null> {
2176
+ try {
2177
+ const endpoint = (await getCurrentAuthContext()) as unknown as {
2178
+ context?: { adapter?: { findOne: (q: unknown) => Promise<unknown> } };
2179
+ };
2180
+ const adapter = endpoint.context?.adapter;
2181
+ if (!adapter) return null;
2182
+
2183
+ const order = (await adapter.findOne({
2184
+ model: "order",
2185
+ where: [{ field: "id", value: orderId }],
2186
+ })) as { userId?: string | null } | null;
2187
+ if (!order?.userId) return null;
2188
+
2189
+ const user = (await adapter.findOne({
2190
+ model: "user",
2191
+ where: [{ field: "id", value: order.userId }],
2192
+ })) as { email?: string | null } | null;
2193
+ return user?.email ?? null;
2194
+ } catch {
2195
+ return null;
2196
+ }
2197
+ }
2141
2198
  `;
2142
2199
  }
2143
2200
  function renderErpTs() {
@@ -340,12 +340,6 @@ var COMMERCE_OPERATIONS = {
340
340
  hint: "order confirmations and shipping updates, queued and retried",
341
341
  required: true
342
342
  },
343
- {
344
- id: "nft",
345
- label: "Token instead of a parcel",
346
- hint: "a buyer who would rather not have the object can take an NFT of it instead — same price, same order, nothing shipped. Needs a mint provider before it mints anything",
347
- recommended: false
348
- },
349
343
  {
350
344
  id: "events",
351
345
  label: "Webhooks",
@@ -445,8 +439,8 @@ var TAX = {
445
439
  };
446
440
  var CARRIERS = {
447
441
  id: "carriers",
448
- title: "Carriers for calculated shipping rates",
449
- intro: "Optional. Flat-rate shipping needs none of these.",
442
+ title: "How an order reaches a buyer",
443
+ intro: "Live rates at checkout, quoted per carrier. Flat-rate shipping needs none of these.",
450
444
  kind: "multiselect",
451
445
  choices: [
452
446
  { id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [{ key: "SHIPPO_API_KEY", breaks: "calculated rates fall back to flat" }] },
@@ -458,7 +452,12 @@ var CARRIERS = {
458
452
  { id: "royal-mail", label: "Royal Mail", hint: "UK post, for a shop whose customers are mostly in Britain", env: [{ key: "ROYAL_MAIL_CLIENT_ID", breaks: "Royal Mail rates are not quoted" }] },
459
453
  { id: "aramex", label: "Aramex", hint: "the carrier to reach the Gulf and wider Middle East reliably", env: [{ key: "ARAMEX_API_KEY", breaks: "Aramex rates are not quoted" }] },
460
454
  { id: "doordash-drive", label: "DoorDash Drive", hint: "same-day local delivery", env: [{ key: "DOORDASH_DEVELOPER_ID", breaks: "DoorDash is not offered" }] },
461
- { id: "stuart", label: "Stuart", hint: "same-day courier, Europe", env: [{ key: "STUART_CLIENT_ID", breaks: "Stuart is not offered" }] }
455
+ { id: "stuart", label: "Stuart", hint: "same-day courier, Europe", env: [{ key: "STUART_CLIENT_ID", breaks: "Stuart is not offered" }] },
456
+ {
457
+ id: "nft",
458
+ label: "A token, not a parcel",
459
+ hint: "the buyer takes an NFT of what they bought instead of the object — quotes free, ships nothing, needs no address. It mints to their email, so they need no wallet"
460
+ }
462
461
  ]
463
462
  };
464
463
  var ERP = {
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ import {
57
57
  routeProblem,
58
58
  strictDependencies,
59
59
  upsertJsonc
60
- } from "./index-wzy1xtr1.js";
60
+ } from "./index-9x9q9ng2.js";
61
61
  import {
62
62
  LAYOUTS,
63
63
  MANIFEST_FILE,
@@ -77,7 +77,7 @@ import {
77
77
  writeManifest,
78
78
  zone,
79
79
  zoneOf
80
- } from "./index-ssv3a6wc.js";
80
+ } from "./index-0vt6g4cb.js";
81
81
  import {
82
82
  AUTH_PLUGINS,
83
83
  CARRIERS,
@@ -94,7 +94,7 @@ import {
94
94
  SIGN_IN,
95
95
  TAX,
96
96
  UI
97
- } from "./index-s7sq41qs.js";
97
+ } from "./index-yk361vn2.js";
98
98
  export {
99
99
  zoneOf,
100
100
  zone,
package/dist/manifest.js CHANGED
@@ -17,8 +17,8 @@ import {
17
17
  writeManifest,
18
18
  zone,
19
19
  zoneOf
20
- } from "./index-ssv3a6wc.js";
21
- import"./index-s7sq41qs.js";
20
+ } from "./index-0vt6g4cb.js";
21
+ import"./index-yk361vn2.js";
22
22
  export {
23
23
  zoneOf,
24
24
  zone,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saastemly/voidcommerce",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/catalog.ts CHANGED
@@ -404,12 +404,6 @@ export const COMMERCE_OPERATIONS: Group = {
404
404
  hint: "order confirmations and shipping updates, queued and retried",
405
405
  required: true,
406
406
  },
407
- {
408
- id: "nft",
409
- label: "Token instead of a parcel",
410
- hint: "a buyer who would rather not have the object can take an NFT of it instead — same price, same order, nothing shipped. Needs a mint provider before it mints anything",
411
- recommended: false,
412
- },
413
407
  {
414
408
  id: "events",
415
409
  label: "Webhooks",
@@ -515,8 +509,8 @@ export const TAX: Group = {
515
509
 
516
510
  export const CARRIERS: Group = {
517
511
  id: "carriers",
518
- title: "Carriers for calculated shipping rates",
519
- intro: "Optional. Flat-rate shipping needs none of these.",
512
+ title: "How an order reaches a buyer",
513
+ intro: "Live rates at checkout, quoted per carrier. Flat-rate shipping needs none of these.",
520
514
  kind: "multiselect",
521
515
  choices: [
522
516
  { id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [{ key: "SHIPPO_API_KEY", breaks: "calculated rates fall back to flat" }] },
@@ -529,6 +523,11 @@ export const CARRIERS: Group = {
529
523
  { id: "aramex", label: "Aramex", hint: "the carrier to reach the Gulf and wider Middle East reliably", env: [{ key: "ARAMEX_API_KEY", breaks: "Aramex rates are not quoted" }] },
530
524
  { id: "doordash-drive", label: "DoorDash Drive", hint: "same-day local delivery", env: [{ key: "DOORDASH_DEVELOPER_ID", breaks: "DoorDash is not offered" }] },
531
525
  { id: "stuart", label: "Stuart", hint: "same-day courier, Europe", env: [{ key: "STUART_CLIENT_ID", breaks: "Stuart is not offered" }] },
526
+ {
527
+ id: "nft",
528
+ label: "A token, not a parcel",
529
+ hint: "the buyer takes an NFT of what they bought instead of the object — quotes free, ships nothing, needs no address. It mints to their email, so they need no wallet",
530
+ },
532
531
  ],
533
532
  };
534
533
 
@@ -60,11 +60,37 @@ export function renderAuthTs(manifest: Manifest): string {
60
60
  * the intent and mints nothing, which is the honest state for a shop that
61
61
  * has not chosen a chain.
62
62
  */
63
+ /**
64
+ * Carriers. Each chosen one becomes a `calculated` shipping option naming
65
+ * it, so the buyer picks a delivery method at checkout and the total
66
+ * follows — including the token, which quotes zero and ships nothing.
67
+ */
68
+ const CARRIER_CALLS: Record<string, { call: string; label: string; import: string }> = {
69
+ "dhl-express": {
70
+ label: "DHL Express",
71
+ call: `dhlExpressProvider({ apiKey: must(env, "DHL_API_KEY") })`,
72
+ import: `import { dhlExpressProvider } from "@saastemly/better-commerce/providers/dhl-express";`,
73
+ },
74
+ shippo: { label: "Shippo", call: `shippoProvider({ apiKey: must(env, "SHIPPO_API_KEY") })`, import: `import { shippoProvider } from "@saastemly/better-commerce/providers/shippo";` },
75
+ easypost: { label: "EasyPost", call: `easypostProvider({ apiKey: must(env, "EASYPOST_API_KEY") })`, import: `import { easypostProvider } from "@saastemly/better-commerce/providers/easypost";` },
76
+ ups: { label: "UPS", call: `upsProvider({ clientId: must(env, "UPS_CLIENT_ID"), clientSecret: must(env, "UPS_CLIENT_SECRET") })`, import: `import { upsProvider } from "@saastemly/better-commerce/providers/ups";` },
77
+ fedex: { label: "FedEx", call: `fedexProvider({ clientId: must(env, "FEDEX_CLIENT_ID"), clientSecret: must(env, "FEDEX_CLIENT_SECRET") })`, import: `import { fedexProvider } from "@saastemly/better-commerce/providers/fedex";` },
78
+ "royal-mail": { label: "Royal Mail", call: `royalMailProvider({ clientId: must(env, "ROYAL_MAIL_CLIENT_ID") })`, import: `import { royalMailProvider } from "@saastemly/better-commerce/providers/royal-mail";` },
79
+ nft: {
80
+ label: "A token, not a parcel",
81
+ call: `nftDelivery({ mint: mintProvider, destinationFor })`,
82
+ import: `import { nftDelivery } from "@saastemly/better-commerce/providers/nft-delivery";\nimport { mintProvider, destinationFor } from "./lib/mint.ts";`,
83
+ },
84
+ };
85
+ const carriers = Object.keys(CARRIER_CALLS).filter((id) => has(manifest, id));
86
+ const carrierImports = carriers.map((id) => CARRIER_CALLS[id]!.import);
63
87
  const wantsNft = has(manifest, "nft");
64
- const nftProvider = wantsNft
65
- ? `, nftOrPhysical({ mint: mintProvider, physical: ${has(manifest, "business-central") ? `erpFulfillment("${shop.currency.toUpperCase()}")` : "manualFulfillment()"} })`
88
+ // One bridge for every carrier: which ships is decided by whose rate the
89
+ // buyer paid for, so a token order mints and a parcel order posts.
90
+ const carrierFulfillment = carriers.length
91
+ ? `, deliveryFulfillment([${carriers.map((id) => CARRIER_CALLS[id]!.call).join(", ")}])`
66
92
  : "";
67
- const nftDefault = wantsNft ? `, defaultProvider: "nft-or-physical"` : "";
93
+ const nftDefault = carriers.length ? `, defaultProvider: "delivery:${carriers.join("+")}"` : "";
68
94
 
69
95
  const vat = VAT[shop.country];
70
96
 
@@ -197,13 +223,22 @@ export function renderAuthTs(manifest: Manifest): string {
197
223
  commerce("acp", `acp({ merchantDisplayName: "${shop.name}" })`, "agentic commerce", `import { acp } from "@saastemly/better-commerce/acp";`);
198
224
  commerce(
199
225
  "fulfillments",
200
- `fulfillments({ providers: [authFulfillment(), creditsFulfillment()${has(manifest, "gift-cards") ? ", giftCardFulfillment()" : ""}${has(manifest, "business-central") ? `, erpFulfillment("${shop.currency.toUpperCase()}")` : ""}${nftProvider}]${nftDefault} })`,
226
+ `fulfillments({ providers: [authFulfillment(), creditsFulfillment()${has(manifest, "gift-cards") ? ", giftCardFulfillment()" : ""}${has(manifest, "business-central") ? `, erpFulfillment("${shop.currency.toUpperCase()}")` : ""}${carrierFulfillment}]${nftDefault} })`,
201
227
  "what happens when the money is confirmed",
202
- `import { authFulfillment, creditsFulfillment, fulfillments, manualFulfillment } from "@saastemly/better-commerce/plugins/fulfillments";${has(manifest, "business-central") ? `\nimport { erpFulfillment } from "./lib/erp.ts";` : ""}`,
228
+ `import { authFulfillment, creditsFulfillment, fulfillments, manualFulfillment } from "@saastemly/better-commerce/plugins/fulfillments";${carriers.length ? `\nimport { deliveryFulfillment } from "@saastemly/better-commerce/providers/delivery";` : ""}${carrierImports.length ? `\n${carrierImports.join("\n")}` : ""}${has(manifest, "business-central") ? `\nimport { erpFulfillment } from "./lib/erp.ts";` : ""}`,
203
229
  );
204
230
  commerce(
205
231
  "shipping-zones",
206
- `shippingZones({ zones: [{ id: "domestic", name: "${shop.country}", countries: ["${shop.country}"], rank: 0 }], shippingOptions: [{ id: "standard", zoneId: "domestic", name: "Standard", priceType: "flat", amount: Number(must(env, "SHIPPING_DOMESTIC_MINOR")), currency: "${shop.currency}", rank: 0 }, { id: "free", zoneId: "domestic", name: "Free", priceType: "free", currency: "${shop.currency}", minSubtotal: Number(must(env, "SHIPPING_FREE_FROM_MINOR")), rank: 1 }] })`,
232
+ `shippingZones({ zones: [{ id: "domestic", name: "${shop.country}", countries: ["${shop.country}"], rank: 0 }], shippingOptions: [${
233
+ carriers.length === 0
234
+ ? `{ id: "standard", zoneId: "domestic", name: "Standard", priceType: "flat", amount: Number(must(env, "SHIPPING_DOMESTIC_MINOR")), currency: "${shop.currency}", rank: 0 }, { id: "free", zoneId: "domestic", name: "Free", priceType: "free", currency: "${shop.currency}", minSubtotal: Number(must(env, "SHIPPING_FREE_FROM_MINOR")), rank: 1 }`
235
+ : carriers
236
+ .map(
237
+ (id, rank) =>
238
+ `{ id: "${id}", zoneId: "domestic", name: "${CARRIER_CALLS[id]!.label}", priceType: "calculated", carrierProviderId: "${id === "nft" ? "nft" : id}", currency: "${shop.currency}", rank: ${rank} }`,
239
+ )
240
+ .join(", ")
241
+ }]${carriers.length ? `, carriers: [${carriers.map((id) => CARRIER_CALLS[id]!.call).join(", ")}]` : ""} })`,
207
242
  "quoted from required env; a shop with no option quotes FREE delivery silently",
208
243
  );
209
244
  /**
@@ -247,7 +282,7 @@ export function renderAuthTs(manifest: Manifest): string {
247
282
  commerce("merchants", `merchants()`, "several sellers, with payouts");
248
283
  if (has(manifest, "nft"))
249
284
  lines.push({
250
- import: `import { nft, nftOrPhysical } from "@saastemly/better-commerce/plugins/nft";\nimport { mintProvider } from "./lib/mint.ts";`,
285
+ import: `import { nft } from "@saastemly/better-commerce/plugins/nft";`,
251
286
  call: `nft({ blurb: "Rather not have the object? Take an authentic, completely useless token of it instead. Nothing gets printed, nothing gets posted." })`,
252
287
  ring: "commerce",
253
288
  why: "the buyer chooses a parcel or a token; the catalogue is the same either way",
@@ -220,6 +220,41 @@ export function renderMintTs(): string {
220
220
  * after payment, and an exception there strands a paid order.
221
221
  */
222
222
  export const mintProvider: MintProvider = recordedMint();
223
+
224
+ /**
225
+ * Where THIS order's token goes.
226
+ *
227
+ * The buyer's own email, because a custodial mint provider will make them a
228
+ * wallet from it — so somebody who has never held one still receives the
229
+ * token, and the checkout never has to ask for a wallet address. Returning
230
+ * null refuses the shipment rather than minting into the void.
231
+ *
232
+ * The adapter is reached the way the emailer reaches it: through the current
233
+ * auth context, which exists because this runs inside a request.
234
+ */
235
+ export async function destinationFor(orderId: string): Promise<string | null> {
236
+ try {
237
+ const endpoint = (await getCurrentAuthContext()) as unknown as {
238
+ context?: { adapter?: { findOne: (q: unknown) => Promise<unknown> } };
239
+ };
240
+ const adapter = endpoint.context?.adapter;
241
+ if (!adapter) return null;
242
+
243
+ const order = (await adapter.findOne({
244
+ model: "order",
245
+ where: [{ field: "id", value: orderId }],
246
+ })) as { userId?: string | null } | null;
247
+ if (!order?.userId) return null;
248
+
249
+ const user = (await adapter.findOne({
250
+ model: "user",
251
+ where: [{ field: "id", value: order.userId }],
252
+ })) as { email?: string | null } | null;
253
+ return user?.email ?? null;
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
223
258
  `;
224
259
  }
225
260