@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 +1 -1
- package/dist/cli.js +3 -3
- package/dist/{index-ssv3a6wc.js → index-0vt6g4cb.js} +1 -1
- package/dist/{index-wzy1xtr1.js → index-9x9q9ng2.js} +66 -9
- package/dist/{index-s7sq41qs.js → index-yk361vn2.js} +8 -9
- package/dist/index.js +3 -3
- package/dist/manifest.js +2 -2
- package/package.json +1 -1
- package/src/catalog.ts +7 -8
- package/src/generate/auth.ts +42 -7
- package/src/generate/support.ts +35 -0
package/dist/catalog.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -22,16 +22,16 @@ import {
|
|
|
22
22
|
runVoid,
|
|
23
23
|
version,
|
|
24
24
|
voidAppsIn
|
|
25
|
-
} from "./index-
|
|
25
|
+
} from "./index-9x9q9ng2.js";
|
|
26
26
|
import {
|
|
27
27
|
LAYOUTS,
|
|
28
28
|
readManifest,
|
|
29
29
|
validate,
|
|
30
30
|
zoneOf
|
|
31
|
-
} from "./index-
|
|
31
|
+
} from "./index-0vt6g4cb.js";
|
|
32
32
|
import {
|
|
33
33
|
GROUPS
|
|
34
|
-
} from "./index-
|
|
34
|
+
} from "./index-yk361vn2.js";
|
|
35
35
|
|
|
36
36
|
// src/deploy/index.ts
|
|
37
37
|
import color from "picocolors";
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
workerHosts,
|
|
12
12
|
writeManifest,
|
|
13
13
|
zone
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-0vt6g4cb.js";
|
|
15
15
|
import {
|
|
16
16
|
CHOICES,
|
|
17
17
|
__require
|
|
18
|
-
} from "./index-
|
|
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
|
|
43
|
-
const nftDefault =
|
|
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()}")` : ""}${
|
|
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
|
|
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.
|
|
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: "
|
|
449
|
-
intro: "
|
|
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-
|
|
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-
|
|
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-
|
|
97
|
+
} from "./index-yk361vn2.js";
|
|
98
98
|
export {
|
|
99
99
|
zoneOf,
|
|
100
100
|
zone,
|
package/dist/manifest.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saastemly/voidcommerce",
|
|
3
|
-
"version": "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: "
|
|
519
|
-
intro: "
|
|
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
|
|
package/src/generate/auth.ts
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
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 =
|
|
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()}")` : ""}${
|
|
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: [
|
|
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
|
|
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",
|
package/src/generate/support.ts
CHANGED
|
@@ -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
|
|