@saastemly/voidcommerce 0.1.0 → 0.2.1
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/generate/support.d.ts +13 -0
- package/dist/{index-wzy1xtr1.js → index-xdsp97e4.js} +105 -9
- package/dist/{index-s7sq41qs.js → index-z4qazajn.js} +12 -13
- package/dist/{index-ssv3a6wc.js → index-zh3tmj08.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/manifest.js +2 -2
- package/package.json +1 -1
- package/src/catalog.ts +11 -12
- package/src/generate/auth.ts +61 -7
- package/src/generate/index.ts +5 -1
- package/src/generate/support.ts +67 -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-xdsp97e4.js";
|
|
26
26
|
import {
|
|
27
27
|
LAYOUTS,
|
|
28
28
|
readManifest,
|
|
29
29
|
validate,
|
|
30
30
|
zoneOf
|
|
31
|
-
} from "./index-
|
|
31
|
+
} from "./index-zh3tmj08.js";
|
|
32
32
|
import {
|
|
33
33
|
GROUPS
|
|
34
|
-
} from "./index-
|
|
34
|
+
} from "./index-z4qazajn.js";
|
|
35
35
|
|
|
36
36
|
// src/deploy/index.ts
|
|
37
37
|
import color from "picocolors";
|
|
@@ -20,4 +20,17 @@ export declare function renderNotificationsTs(): string;
|
|
|
20
20
|
* because the choice lives on the line, not in the catalogue.
|
|
21
21
|
*/
|
|
22
22
|
export declare function renderMintTs(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Where parcels leave from. Written once, then yours.
|
|
25
|
+
*
|
|
26
|
+
* Every carrier needs an origin to quote against, and it is shop data rather
|
|
27
|
+
* than a credential — a postcode changes when you move, not when a key
|
|
28
|
+
* rotates. So it is a file you edit, not an env var, and it ships with your
|
|
29
|
+
* own details already in the shape the carriers want.
|
|
30
|
+
*
|
|
31
|
+
* DHL in particular rejects a shipment whose origin has no name and phone,
|
|
32
|
+
* which is a failure that shows up at the first real order rather than at
|
|
33
|
+
* deploy, so both are marked rather than left blank.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderShippingTs(manifest: Manifest): string;
|
|
23
36
|
export declare function renderErpTs(): string;
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
workerHosts,
|
|
12
12
|
writeManifest,
|
|
13
13
|
zone
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-zh3tmj08.js";
|
|
15
15
|
import {
|
|
16
16
|
CHOICES,
|
|
17
17
|
__require
|
|
18
|
-
} from "./index-
|
|
18
|
+
} from "./index-z4qazajn.js";
|
|
19
19
|
|
|
20
20
|
// src/generate/auth.ts
|
|
21
21
|
var VAT = {
|
|
@@ -38,9 +38,46 @@ 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"), apiSecret: must(env, "DHL_API_SECRET"), accountNumber: must(env, "DHL_ACCOUNT_NUMBER"), from: shipFrom })`,
|
|
45
|
+
import: `import { dhlExpressProvider } from "@saastemly/better-commerce/providers/dhl-express";`
|
|
46
|
+
},
|
|
47
|
+
shippo: {
|
|
48
|
+
label: "Shippo",
|
|
49
|
+
call: `shippoProvider({ apiToken: must(env, "SHIPPO_API_TOKEN"), from: shipFrom })`,
|
|
50
|
+
import: `import { shippoProvider } from "@saastemly/better-commerce/providers/shippo";`
|
|
51
|
+
},
|
|
52
|
+
easypost: {
|
|
53
|
+
label: "EasyPost",
|
|
54
|
+
call: `easypostProvider({ apiKey: must(env, "EASYPOST_API_KEY"), from: shipFrom })`,
|
|
55
|
+
import: `import { easypostProvider } from "@saastemly/better-commerce/providers/easypost";`
|
|
56
|
+
},
|
|
57
|
+
ups: {
|
|
58
|
+
label: "UPS",
|
|
59
|
+
call: `upsProvider({ clientId: must(env, "UPS_CLIENT_ID"), clientSecret: must(env, "UPS_CLIENT_SECRET"), accountNumber: must(env, "UPS_ACCOUNT_NUMBER"), from: shipFrom })`,
|
|
60
|
+
import: `import { upsProvider } from "@saastemly/better-commerce/providers/ups";`
|
|
61
|
+
},
|
|
62
|
+
fedex: {
|
|
63
|
+
label: "FedEx",
|
|
64
|
+
call: `fedexProvider({ clientId: must(env, "FEDEX_CLIENT_ID"), clientSecret: must(env, "FEDEX_CLIENT_SECRET"), accountNumber: must(env, "FEDEX_ACCOUNT_NUMBER"), from: shipFrom })`,
|
|
65
|
+
import: `import { fedexProvider } from "@saastemly/better-commerce/providers/fedex";`
|
|
66
|
+
},
|
|
67
|
+
nft: {
|
|
68
|
+
label: "A token, not a parcel",
|
|
69
|
+
call: `nftDelivery({ mint: mintProvider, destinationFor })`,
|
|
70
|
+
import: `import { nftDelivery } from "@saastemly/better-commerce/providers/nft-delivery";
|
|
71
|
+
import { mintProvider, destinationFor } from "./lib/mint.ts";`
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const carriers = Object.keys(CARRIER_CALLS).filter((id) => has(manifest, id));
|
|
75
|
+
const carrierImports = carriers.map((id) => CARRIER_CALLS[id].import);
|
|
76
|
+
if (carriers.some((id) => id !== "nft"))
|
|
77
|
+
carrierImports.push(`import { shipFrom } from "./lib/shipping.ts";`);
|
|
41
78
|
const wantsNft = has(manifest, "nft");
|
|
42
|
-
const
|
|
43
|
-
const nftDefault =
|
|
79
|
+
const carrierFulfillment = carriers.length ? `, deliveryFulfillment([${carriers.map((id) => CARRIER_CALLS[id].call).join(", ")}])` : "";
|
|
80
|
+
const nftDefault = carriers.length ? `, defaultProvider: "delivery:${carriers.join("+")}"` : "";
|
|
44
81
|
const vat = VAT[shop.country];
|
|
45
82
|
lines.push({
|
|
46
83
|
import: `import { admin } from "better-auth/plugins/admin";`,
|
|
@@ -168,9 +205,12 @@ import { memorySearchProvider } from "@saastemly/better-commerce/providers/searc
|
|
|
168
205
|
commerce("reviews", `reviews()`, "verified badge derived from a paid order");
|
|
169
206
|
commerce("customer-groups", `customerGroups()`, "segments with their own prices");
|
|
170
207
|
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()}")` : ""}${
|
|
208
|
+
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 ? `
|
|
209
|
+
import { deliveryFulfillment } from "@saastemly/better-commerce/providers/delivery";` : ""}${carrierImports.length ? `
|
|
210
|
+
${carrierImports.join(`
|
|
211
|
+
`)}` : ""}${has(manifest, "business-central") ? `
|
|
172
212
|
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");
|
|
213
|
+
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
214
|
const registered = shop.taxRegistered === true;
|
|
175
215
|
const askedLive = has(manifest, "stripe-tax");
|
|
176
216
|
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 +231,7 @@ import { memoryFileProvider } from "@saastemly/better-commerce/providers/file";`
|
|
|
191
231
|
commerce("merchants", `merchants()`, "several sellers, with payouts");
|
|
192
232
|
if (has(manifest, "nft"))
|
|
193
233
|
lines.push({
|
|
194
|
-
import: `import { nft
|
|
195
|
-
import { mintProvider } from "./lib/mint.ts";`,
|
|
234
|
+
import: `import { nft } from "@saastemly/better-commerce/plugins/nft";`,
|
|
196
235
|
call: `nft({ blurb: "Rather not have the object? Take an authentic, completely useless token of it instead. Nothing gets printed, nothing gets posted." })`,
|
|
197
236
|
ring: "commerce",
|
|
198
237
|
why: "the buyer chooses a parcel or a token; the catalogue is the same either way"
|
|
@@ -1397,7 +1436,7 @@ import color from "picocolors";
|
|
|
1397
1436
|
// package.json
|
|
1398
1437
|
var package_default = {
|
|
1399
1438
|
name: "@saastemly/voidcommerce",
|
|
1400
|
-
version: "0.1
|
|
1439
|
+
version: "0.2.1",
|
|
1401
1440
|
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
1441
|
type: "module",
|
|
1403
1442
|
license: "MIT",
|
|
@@ -2138,6 +2177,60 @@ function renderMintTs() {
|
|
|
2138
2177
|
* after payment, and an exception there strands a paid order.
|
|
2139
2178
|
*/
|
|
2140
2179
|
export const mintProvider: MintProvider = recordedMint();
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Where THIS order's token goes.
|
|
2183
|
+
*
|
|
2184
|
+
* The buyer's own email, because a custodial mint provider will make them a
|
|
2185
|
+
* wallet from it — so somebody who has never held one still receives the
|
|
2186
|
+
* token, and the checkout never has to ask for a wallet address. Returning
|
|
2187
|
+
* null refuses the shipment rather than minting into the void.
|
|
2188
|
+
*
|
|
2189
|
+
* The adapter is reached the way the emailer reaches it: through the current
|
|
2190
|
+
* auth context, which exists because this runs inside a request.
|
|
2191
|
+
*/
|
|
2192
|
+
export async function destinationFor(orderId: string): Promise<string | null> {
|
|
2193
|
+
try {
|
|
2194
|
+
const endpoint = (await getCurrentAuthContext()) as unknown as {
|
|
2195
|
+
context?: { adapter?: { findOne: (q: unknown) => Promise<unknown> } };
|
|
2196
|
+
};
|
|
2197
|
+
const adapter = endpoint.context?.adapter;
|
|
2198
|
+
if (!adapter) return null;
|
|
2199
|
+
|
|
2200
|
+
const order = (await adapter.findOne({
|
|
2201
|
+
model: "order",
|
|
2202
|
+
where: [{ field: "id", value: orderId }],
|
|
2203
|
+
})) as { userId?: string | null } | null;
|
|
2204
|
+
if (!order?.userId) return null;
|
|
2205
|
+
|
|
2206
|
+
const user = (await adapter.findOne({
|
|
2207
|
+
model: "user",
|
|
2208
|
+
where: [{ field: "id", value: order.userId }],
|
|
2209
|
+
})) as { email?: string | null } | null;
|
|
2210
|
+
return user?.email ?? null;
|
|
2211
|
+
} catch {
|
|
2212
|
+
return null;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
`;
|
|
2216
|
+
}
|
|
2217
|
+
function renderShippingTs(manifest) {
|
|
2218
|
+
return `import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";
|
|
2219
|
+
|
|
2220
|
+
/**
|
|
2221
|
+
* The address every parcel is quoted and shipped FROM.
|
|
2222
|
+
*
|
|
2223
|
+
* TODO VERIFY: this is a placeholder. A carrier quotes against it, and DHL
|
|
2224
|
+
* refuses a shipment whose origin has no \`name\` and \`phone\`.
|
|
2225
|
+
*/
|
|
2226
|
+
export const shipFrom: DeliveryAddress = {
|
|
2227
|
+
name: "${manifest.shop.name}",
|
|
2228
|
+
line1: "TODO VERIFY: street",
|
|
2229
|
+
city: "TODO VERIFY: town",
|
|
2230
|
+
postalCode: "TODO VERIFY",
|
|
2231
|
+
country: "${manifest.shop.country}",
|
|
2232
|
+
phone: "TODO VERIFY: +44…",
|
|
2233
|
+
};
|
|
2141
2234
|
`;
|
|
2142
2235
|
}
|
|
2143
2236
|
function renderErpTs() {
|
|
@@ -2248,6 +2341,9 @@ async function generateApi(root, dir, manifest, result, opts) {
|
|
|
2248
2341
|
if (has(manifest, "nft")) {
|
|
2249
2342
|
await put(root, at("lib/mint.ts"), renderMintTs(), result, own);
|
|
2250
2343
|
}
|
|
2344
|
+
if (["dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id))) {
|
|
2345
|
+
await put(root, at("lib/shipping.ts"), renderShippingTs(manifest), result, own);
|
|
2346
|
+
}
|
|
2251
2347
|
if (has(manifest, "business-central")) {
|
|
2252
2348
|
await put(root, at("lib/erp.ts"), renderErpTs(), result, own);
|
|
2253
2349
|
}
|
|
@@ -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,20 +439,25 @@ 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
|
-
{ id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [{ key: "
|
|
446
|
+
{ id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [{ key: "SHIPPO_API_TOKEN", breaks: "calculated rates fall back to flat" }] },
|
|
453
447
|
{ id: "easypost", label: "EasyPost", hint: "the same, US-leaning", env: [{ key: "EASYPOST_API_KEY", breaks: "calculated rates fall back to flat" }] },
|
|
454
|
-
{ id: "ups", label: "UPS", hint: "your own UPS account, for negotiated rates an aggregator cannot see", env: [{ key: "UPS_CLIENT_ID", breaks: "UPS rates are not quoted" }, { key: "UPS_CLIENT_SECRET", breaks: "the same" }] },
|
|
455
|
-
{ id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" }, { key: "FEDEX_CLIENT_SECRET", breaks: "the same" }] },
|
|
456
|
-
{ id: "dhl-express", label: "DHL Express", hint: "international", env: [{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" }] },
|
|
448
|
+
{ id: "ups", label: "UPS", hint: "your own UPS account, for negotiated rates an aggregator cannot see", env: [{ key: "UPS_CLIENT_ID", breaks: "UPS rates are not quoted" }, { key: "UPS_CLIENT_SECRET", breaks: "the same" }, { key: "UPS_ACCOUNT_NUMBER", breaks: "UPS has no account to bill the transport to" }] },
|
|
449
|
+
{ id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" }, { key: "FEDEX_CLIENT_SECRET", breaks: "the same" }, { key: "FEDEX_ACCOUNT_NUMBER", breaks: "FedEx has no account to bill the transport to" }] },
|
|
450
|
+
{ id: "dhl-express", label: "DHL Express", hint: "international express, and the one worth having for a shop that posts abroad", env: [{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" }, { key: "DHL_API_SECRET", breaks: "the same" }, { key: "DHL_ACCOUNT_NUMBER", breaks: "DHL has no account to bill the transport to" }] },
|
|
457
451
|
{ id: "usps", label: "USPS", hint: "US domestic post — the cheapest way to ship a small parcel inside the US", env: [{ key: "USPS_CLIENT_ID", breaks: "USPS rates are not quoted" }, { key: "USPS_CLIENT_SECRET", breaks: "the same" }] },
|
|
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-xdsp97e4.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-zh3tmj08.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-z4qazajn.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.1
|
|
3
|
+
"version": "0.2.1",
|
|
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,20 +509,25 @@ 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
|
-
{ id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [{ key: "
|
|
516
|
+
{ id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [{ key: "SHIPPO_API_TOKEN", breaks: "calculated rates fall back to flat" }] },
|
|
523
517
|
{ id: "easypost", label: "EasyPost", hint: "the same, US-leaning", env: [{ key: "EASYPOST_API_KEY", breaks: "calculated rates fall back to flat" }] },
|
|
524
|
-
{ id: "ups", label: "UPS", hint: "your own UPS account, for negotiated rates an aggregator cannot see", env: [{ key: "UPS_CLIENT_ID", breaks: "UPS rates are not quoted" }, { key: "UPS_CLIENT_SECRET", breaks: "the same" }] },
|
|
525
|
-
{ id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" }, { key: "FEDEX_CLIENT_SECRET", breaks: "the same" }] },
|
|
526
|
-
{ id: "dhl-express", label: "DHL Express", hint: "international", env: [{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" }] },
|
|
518
|
+
{ id: "ups", label: "UPS", hint: "your own UPS account, for negotiated rates an aggregator cannot see", env: [{ key: "UPS_CLIENT_ID", breaks: "UPS rates are not quoted" }, { key: "UPS_CLIENT_SECRET", breaks: "the same" }, { key: "UPS_ACCOUNT_NUMBER", breaks: "UPS has no account to bill the transport to" }] },
|
|
519
|
+
{ id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" }, { key: "FEDEX_CLIENT_SECRET", breaks: "the same" }, { key: "FEDEX_ACCOUNT_NUMBER", breaks: "FedEx has no account to bill the transport to" }] },
|
|
520
|
+
{ id: "dhl-express", label: "DHL Express", hint: "international express, and the one worth having for a shop that posts abroad", env: [{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" }, { key: "DHL_API_SECRET", breaks: "the same" }, { key: "DHL_ACCOUNT_NUMBER", breaks: "DHL has no account to bill the transport to" }] },
|
|
527
521
|
{ id: "usps", label: "USPS", hint: "US domestic post — the cheapest way to ship a small parcel inside the US", env: [{ key: "USPS_CLIENT_ID", breaks: "USPS rates are not quoted" }, { key: "USPS_CLIENT_SECRET", breaks: "the same" }] },
|
|
528
522
|
{ 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" }] },
|
|
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,56 @@ 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
|
+
// Every real carrier needs the ORIGIN it ships from, which is shop data
|
|
70
|
+
// rather than a credential — so it lives in an owned lib/shipping.ts.
|
|
71
|
+
"dhl-express": {
|
|
72
|
+
label: "DHL Express",
|
|
73
|
+
call: `dhlExpressProvider({ apiKey: must(env, "DHL_API_KEY"), apiSecret: must(env, "DHL_API_SECRET"), accountNumber: must(env, "DHL_ACCOUNT_NUMBER"), from: shipFrom })`,
|
|
74
|
+
import: `import { dhlExpressProvider } from "@saastemly/better-commerce/providers/dhl-express";`,
|
|
75
|
+
},
|
|
76
|
+
shippo: {
|
|
77
|
+
label: "Shippo",
|
|
78
|
+
call: `shippoProvider({ apiToken: must(env, "SHIPPO_API_TOKEN"), from: shipFrom })`,
|
|
79
|
+
import: `import { shippoProvider } from "@saastemly/better-commerce/providers/shippo";`,
|
|
80
|
+
},
|
|
81
|
+
easypost: {
|
|
82
|
+
label: "EasyPost",
|
|
83
|
+
call: `easypostProvider({ apiKey: must(env, "EASYPOST_API_KEY"), from: shipFrom })`,
|
|
84
|
+
import: `import { easypostProvider } from "@saastemly/better-commerce/providers/easypost";`,
|
|
85
|
+
},
|
|
86
|
+
ups: {
|
|
87
|
+
label: "UPS",
|
|
88
|
+
call: `upsProvider({ clientId: must(env, "UPS_CLIENT_ID"), clientSecret: must(env, "UPS_CLIENT_SECRET"), accountNumber: must(env, "UPS_ACCOUNT_NUMBER"), from: shipFrom })`,
|
|
89
|
+
import: `import { upsProvider } from "@saastemly/better-commerce/providers/ups";`,
|
|
90
|
+
},
|
|
91
|
+
fedex: {
|
|
92
|
+
label: "FedEx",
|
|
93
|
+
call: `fedexProvider({ clientId: must(env, "FEDEX_CLIENT_ID"), clientSecret: must(env, "FEDEX_CLIENT_SECRET"), accountNumber: must(env, "FEDEX_ACCOUNT_NUMBER"), from: shipFrom })`,
|
|
94
|
+
import: `import { fedexProvider } from "@saastemly/better-commerce/providers/fedex";`,
|
|
95
|
+
},
|
|
96
|
+
nft: {
|
|
97
|
+
label: "A token, not a parcel",
|
|
98
|
+
call: `nftDelivery({ mint: mintProvider, destinationFor })`,
|
|
99
|
+
import: `import { nftDelivery } from "@saastemly/better-commerce/providers/nft-delivery";\nimport { mintProvider, destinationFor } from "./lib/mint.ts";`,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const carriers = Object.keys(CARRIER_CALLS).filter((id) => has(manifest, id));
|
|
103
|
+
const carrierImports = carriers.map((id) => CARRIER_CALLS[id]!.import);
|
|
104
|
+
// The token needs no origin; every other carrier does.
|
|
105
|
+
if (carriers.some((id) => id !== "nft")) carrierImports.push(`import { shipFrom } from "./lib/shipping.ts";`);
|
|
63
106
|
const wantsNft = has(manifest, "nft");
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
// One bridge for every carrier: which ships is decided by whose rate the
|
|
108
|
+
// buyer paid for, so a token order mints and a parcel order posts.
|
|
109
|
+
const carrierFulfillment = carriers.length
|
|
110
|
+
? `, deliveryFulfillment([${carriers.map((id) => CARRIER_CALLS[id]!.call).join(", ")}])`
|
|
66
111
|
: "";
|
|
67
|
-
const nftDefault =
|
|
112
|
+
const nftDefault = carriers.length ? `, defaultProvider: "delivery:${carriers.join("+")}"` : "";
|
|
68
113
|
|
|
69
114
|
const vat = VAT[shop.country];
|
|
70
115
|
|
|
@@ -197,13 +242,22 @@ export function renderAuthTs(manifest: Manifest): string {
|
|
|
197
242
|
commerce("acp", `acp({ merchantDisplayName: "${shop.name}" })`, "agentic commerce", `import { acp } from "@saastemly/better-commerce/acp";`);
|
|
198
243
|
commerce(
|
|
199
244
|
"fulfillments",
|
|
200
|
-
`fulfillments({ providers: [authFulfillment(), creditsFulfillment()${has(manifest, "gift-cards") ? ", giftCardFulfillment()" : ""}${has(manifest, "business-central") ? `, erpFulfillment("${shop.currency.toUpperCase()}")` : ""}${
|
|
245
|
+
`fulfillments({ providers: [authFulfillment(), creditsFulfillment()${has(manifest, "gift-cards") ? ", giftCardFulfillment()" : ""}${has(manifest, "business-central") ? `, erpFulfillment("${shop.currency.toUpperCase()}")` : ""}${carrierFulfillment}]${nftDefault} })`,
|
|
201
246
|
"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";` : ""}`,
|
|
247
|
+
`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
248
|
);
|
|
204
249
|
commerce(
|
|
205
250
|
"shipping-zones",
|
|
206
|
-
`shippingZones({ zones: [{ id: "domestic", name: "${shop.country}", countries: ["${shop.country}"], rank: 0 }], shippingOptions: [
|
|
251
|
+
`shippingZones({ zones: [{ id: "domestic", name: "${shop.country}", countries: ["${shop.country}"], rank: 0 }], shippingOptions: [${
|
|
252
|
+
carriers.length === 0
|
|
253
|
+
? `{ 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 }`
|
|
254
|
+
: carriers
|
|
255
|
+
.map(
|
|
256
|
+
(id, rank) =>
|
|
257
|
+
`{ id: "${id}", zoneId: "domestic", name: "${CARRIER_CALLS[id]!.label}", priceType: "calculated", carrierProviderId: "${id === "nft" ? "nft" : id}", currency: "${shop.currency}", rank: ${rank} }`,
|
|
258
|
+
)
|
|
259
|
+
.join(", ")
|
|
260
|
+
}]${carriers.length ? `, carriers: [${carriers.map((id) => CARRIER_CALLS[id]!.call).join(", ")}]` : ""} })`,
|
|
207
261
|
"quoted from required env; a shop with no option quotes FREE delivery silently",
|
|
208
262
|
);
|
|
209
263
|
/**
|
|
@@ -247,7 +301,7 @@ export function renderAuthTs(manifest: Manifest): string {
|
|
|
247
301
|
commerce("merchants", `merchants()`, "several sellers, with payouts");
|
|
248
302
|
if (has(manifest, "nft"))
|
|
249
303
|
lines.push({
|
|
250
|
-
import: `import { nft
|
|
304
|
+
import: `import { nft } from "@saastemly/better-commerce/plugins/nft";`,
|
|
251
305
|
call: `nft({ blurb: "Rather not have the object? Take an authentic, completely useless token of it instead. Nothing gets printed, nothing gets posted." })`,
|
|
252
306
|
ring: "commerce",
|
|
253
307
|
why: "the buyer chooses a parcel or a token; the catalogue is the same either way",
|
package/src/generate/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
renderVoidPatch,
|
|
51
51
|
strictDependencies,
|
|
52
52
|
} from "./strict";
|
|
53
|
-
import { renderDomainTs, renderErpTs, renderMintTs, renderNotificationsTs, renderPaymentTs } from "./support";
|
|
53
|
+
import { renderDomainTs, renderErpTs, renderMintTs, renderNotificationsTs, renderPaymentTs, renderShippingTs } from "./support";
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Turn a manifest into files.
|
|
@@ -173,6 +173,10 @@ async function generateApi(root: string, dir: string, manifest: Manifest, result
|
|
|
173
173
|
if (has(manifest, "nft")) {
|
|
174
174
|
await put(root, at("lib/mint.ts"), renderMintTs(), result, own);
|
|
175
175
|
}
|
|
176
|
+
// Any carrier but the token quotes against an origin, which is shop data.
|
|
177
|
+
if (["dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id))) {
|
|
178
|
+
await put(root, at("lib/shipping.ts"), renderShippingTs(manifest), result, own);
|
|
179
|
+
}
|
|
176
180
|
if (has(manifest, "business-central")) {
|
|
177
181
|
await put(root, at("lib/erp.ts"), renderErpTs(), result, own);
|
|
178
182
|
}
|
package/src/generate/support.ts
CHANGED
|
@@ -220,6 +220,73 @@ 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
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Where parcels leave from. Written once, then yours.
|
|
263
|
+
*
|
|
264
|
+
* Every carrier needs an origin to quote against, and it is shop data rather
|
|
265
|
+
* than a credential — a postcode changes when you move, not when a key
|
|
266
|
+
* rotates. So it is a file you edit, not an env var, and it ships with your
|
|
267
|
+
* own details already in the shape the carriers want.
|
|
268
|
+
*
|
|
269
|
+
* DHL in particular rejects a shipment whose origin has no name and phone,
|
|
270
|
+
* which is a failure that shows up at the first real order rather than at
|
|
271
|
+
* deploy, so both are marked rather than left blank.
|
|
272
|
+
*/
|
|
273
|
+
export function renderShippingTs(manifest: Manifest): string {
|
|
274
|
+
return `import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The address every parcel is quoted and shipped FROM.
|
|
278
|
+
*
|
|
279
|
+
* TODO VERIFY: this is a placeholder. A carrier quotes against it, and DHL
|
|
280
|
+
* refuses a shipment whose origin has no \`name\` and \`phone\`.
|
|
281
|
+
*/
|
|
282
|
+
export const shipFrom: DeliveryAddress = {
|
|
283
|
+
name: "${manifest.shop.name}",
|
|
284
|
+
line1: "TODO VERIFY: street",
|
|
285
|
+
city: "TODO VERIFY: town",
|
|
286
|
+
postalCode: "TODO VERIFY",
|
|
287
|
+
country: "${manifest.shop.country}",
|
|
288
|
+
phone: "TODO VERIFY: +44…",
|
|
289
|
+
};
|
|
223
290
|
`;
|
|
224
291
|
}
|
|
225
292
|
|