@develemit/commerce-server 0.4.0 → 0.7.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/docs/admin.md CHANGED
@@ -5,22 +5,29 @@ Part of [`@develemit/commerce-server`](../README.md).
5
5
  Pass `admin: { authorize, storage }` to `commercePlugin` to register product
6
6
  management under `/admin`. Without it, no admin routes exist (404).
7
7
 
8
+ - `requirePhotoToPublish` (default `true`) makes `PATCH` to `status: 'active'`
9
+ refuse a draft with no image. Set `false` for shops that go live without
10
+ photos.
8
11
  - `authorize(req)` returns `true` to allow the request; `false` gives a 401.
9
12
  The kit owns no identity: check a session, token or header yourself.
10
13
  - `storage` implements `FileStorage` (`put(key, stream, contentType)` and
11
14
  `delete(key)`). Keys look like `products/<productId>/<uuid>.<ext>`.
12
- `createMemoryStorage()` is for tests; a local-disk example lives in
13
- `apps/playground-api/src/storage.ts`.
14
-
15
- | Route | Purpose |
16
- | ---------------------------------------- | ------------------------------------------------------ |
17
- | `GET/POST /admin/products` | List all statuses / create (`adminProductInputSchema`) |
18
- | `GET/PATCH /admin/products/:id` | Read / edit; `{ sold: true }` sets quantity to 0 |
19
- | `POST /admin/products/:id/images` | Multipart `file` (+ optional `alt`), 8MB max |
20
- | `PATCH /admin/products/:id/images/order` | `{ imageIds }`, every image exactly once |
21
- | `PATCH /admin/products/:id/images/:id` | `{ alt }` (trimmed, max 500); 404 for unknown image |
22
- | `DELETE /admin/products/:id/images/:id` | Removes the row and the stored file |
23
- | `GET/PUT /admin/shipping-rates` | `PUT` replaces the full set of rates |
15
+ `createMemoryStorage()` is for tests. `createDiskStorage({ dir, publicUrl })`
16
+ is a development-grade local disk store that rejects keys escaping `dir`
17
+ (`..`, absolute paths, encoded traversal, NUL bytes, symlinks out); serve its
18
+ files with `openStoredFile(dir, key)`, which returns `{ stream, contentType,
19
+ size }` or `null` (answer 404).
20
+
21
+ | Route | Purpose |
22
+ | ---------------------------------------- | -------------------------------------------------------- |
23
+ | `GET/POST /admin/products` | List all statuses / create (`adminProductInputSchema`) |
24
+ | `GET /admin/products/counts` | `{ all, live, draft, sold, archived, incompleteDrafts }` |
25
+ | `GET/PATCH /admin/products/:id` | Read / edit; `{ sold: true }` sets quantity to 0 |
26
+ | `POST /admin/products/:id/images` | Multipart `file` (+ optional `alt`), 8MB max |
27
+ | `PATCH /admin/products/:id/images/order` | `{ imageIds }`, every image exactly once |
28
+ | `PATCH /admin/products/:id/images/:id` | `{ alt }` (trimmed, max 500); 404 for unknown image |
29
+ | `DELETE /admin/products/:id/images/:id` | Removes the row and the stored file |
30
+ | `GET/PUT /admin/shipping-rates` | `PUT` replaces the full set of rates |
24
31
 
25
32
  Stored image keys are `products/<productId>/<uuid>.<ext>` and are never
26
33
  overwritten, so their URLs are safe to cache forever. Serve `FileStorage` URLs
@@ -28,8 +35,31 @@ through a CDN with `Cache-Control: public, max-age=31536000, immutable`, and
28
35
  have it (or the storefront's image adapter) resize.
29
36
 
30
37
  Uploads are checked by magic bytes (jpeg, png, webp, heic), not by filename or
31
- the client's content type. Slugs come from the name at creation and stay
32
- stable on rename. Edits never touch inventory holds.
38
+ the client's content type. Edits never touch inventory holds.
39
+
40
+ ## Drafts and publishing
41
+
42
+ A draft may have no name, price or category (a photo-only draft). It never
43
+ appears in the public catalog. Publishing needs all three, plus a photo unless
44
+ `requirePhotoToPublish` is `false`:
45
+
46
+ - `PATCH` to `status: 'active'` with something missing answers `422`
47
+ `{ error: 'PRODUCT_INCOMPLETE', missing: ['name' | 'priceCents' | 'category' | 'images'] }`.
48
+ `AdminClient` throws `ProductIncompleteError` with `missing`.
49
+ - `POST` with `status: 'active'` and no name, price or category answers `400
50
+ INVALID_INPUT` with one issue per missing field. A photo is not checked on
51
+ create, since a new product has no images yet.
52
+ - The public catalog also skips any active row missing a price or category.
53
+
54
+ Slugs: a nameless draft gets `draft-<first 8 chars of id>`. The first time it
55
+ is given a name the slug becomes `slugify(name)` (made unique with `-2`, `-3`).
56
+ After that it never changes, so renaming a live product keeps shop links
57
+ stable. A product created with a name keeps that slug on rename.
58
+
59
+ Counts: each number equals the total of the matching list filter. `live` is
60
+ `status=active`, `draft` is `status=draft`, `archived` is `status=archived`,
61
+ `sold` is `stock=sold` (any status), and `all` is every non-archived product.
62
+ `incompleteDrafts` counts drafts missing a name, price or category.
33
63
 
34
64
  ## Admin orders API
35
65
 
package/docs/payments.md CHANGED
@@ -5,18 +5,48 @@ Part of [`@develemit/commerce-server`](../README.md).
5
5
  `payment` takes any `PaymentGateway` (`createCheckoutSession`,
6
6
  `verifyWebhook`, `refund`). `refund({ sessionId, amountCents, idempotencyKey })`
7
7
  must return the same `gatewayRefundId` when called again with the same key.
8
+ A gateway may also define `preflight(): Promise<void>`; `commercePlugin` awaits
9
+ it during registration, so a gateway that can't work fails startup.
8
10
  Two ship with the package:
9
11
 
10
12
  - `createStripeGateway({ secretKey, webhookSecret })` loads `stripe` lazily, so
11
13
  it's an optional peer. Refunds look up the session's PaymentIntent and call
12
14
  `refunds.create` with the idempotency key.
13
- - `createFakeGateway()` for tests. Its session URL is the success URL, and
15
+ Registration awaits the optional `preflight()` hook, which loads `stripe`,
16
+ so a missing package fails startup with `pnpm add stripe` instead of the
17
+ first checkout. A passed-in `client` skips the import.
18
+ - `createFakeGateway()` for tests and local development. By default its
19
+ session URL is the success URL (see [Local checkout](#local-checkout-without-stripe)
20
+ for a click-through flow), and
14
21
  `completed(sessionId)` / `expired(sessionId)` / `asyncPaid(sessionId)` /
15
22
  `asyncFailed(sessionId)` return signed webhook bodies to
16
23
  post to the webhook route. `refunds` lists the refunds it made (one per
17
24
  key), `refundCalls` every call, and `failNextRefund()` makes the next one
18
25
  throw.
19
26
 
27
+ ## Local checkout without Stripe
28
+
29
+ Pass `devCheckout: true` (or `{ apiUrl: 'http://localhost:3301' }` when the
30
+ browser reaches the api on another origin) to `commercePlugin` while using the
31
+ fake gateway. Checkout then returns a session URL pointing at
32
+ `GET <prefix>/checkout/dev/:sessionId/complete`, which applies a paid
33
+ `checkout.completed` event through the same path the webhook uses (holds and
34
+ discount redemptions commit, `onOrderPaid` runs) and answers 302 to
35
+ `urls.success(sessionId)`. `?outcome=expired` releases the order instead and
36
+ redirects to `urls.cancel`. The route is only registered for the fake gateway,
37
+ only knows sessions created since the process started (404 otherwise), and
38
+ `commercePlugin` throws at registration when `NODE_ENV=production`. Gate the
39
+ option on `NODE_ENV !== 'production'` so one config works in every
40
+ environment.
41
+
42
+ The completed order looks like a real one: buyer email `dev-buyer@example.com`,
43
+ a sample US address, and the session's first offered shipping rate (so the
44
+ total includes shipping; no rate means no shipping line). Override the buyer
45
+ with `devCheckout: { buyer: { email, name, address } }`, or pass
46
+ `buyer: (sessionId) => ({ ... })` to compute it per session. Omitted fields keep
47
+ their defaults, so `onOrderPaid` hooks that send a confirmation email or buy a
48
+ label can run locally without a custom dev route.
49
+
20
50
  ## Discounts
21
51
 
22
52
  A discount code is an amount off the merchandise subtotal, never shipping. The
@@ -0,0 +1,3 @@
1
+ ALTER TABLE "commerce_products" ALTER COLUMN "name" SET DEFAULT '';--> statement-breakpoint
2
+ ALTER TABLE "commerce_products" ALTER COLUMN "price_cents" DROP NOT NULL;--> statement-breakpoint
3
+ ALTER TABLE "commerce_products" ALTER COLUMN "category" DROP NOT NULL;