@develemit/commerce-server 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +68 -6
- package/dist/index.js.map +1 -1
- package/docs/admin.md +11 -10
- package/migrations/0009_product_published_at.sql +2 -0
- package/migrations/meta/0009_snapshot.json +1035 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.8.0 (2026-11-12)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- `DELETE /admin/products/:id` permanently deletes a draft that has never been live and has no order lines (204), along with its image files and holds; otherwise 409 `PRODUCT_NOT_DELETABLE` with `reason: 'published' | 'has_orders'`. Migration `0009` adds `commerce_products.published_at` (set when a product first goes active; backfilled for existing non-draft products), and admin products report `everPublished`.
|
|
6
|
+
- `orderEmailLines` returns `reference`, the short order reference to print in order emails instead of the full id.
|
|
7
|
+
- `GET /admin/orders?q=` accepts the short reference (with or without the leading `#`).
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
|
|
11
|
+
- `GET /admin/products/counts`: `live` counts active pieces with stock, and `sold` counts active pieces at zero stock, so a sold piece is no longer counted in both. Each chip's count equals the list its filter shows (`status=active&stock=in` / `status=active&stock=sold`). Upgrading shows lower Live numbers.
|
|
12
|
+
|
|
1
13
|
## 0.7.0 (2026-11-04)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/README.md
CHANGED
|
@@ -132,6 +132,9 @@ onOrderPaid: async (order) => {
|
|
|
132
132
|
},
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
`lines.reference` is the short order reference (`#A1B2C3D4`, from
|
|
136
|
+
`formatOrderRef` in contracts); print it instead of the full id.
|
|
137
|
+
|
|
135
138
|
The result also has structured `items`, `subtotal`, `discount` (absent when
|
|
136
139
|
no discount applied), `shipping`, `total` and `address` (absent for pickup).
|
|
137
140
|
HTML output is `<tr>` / `<p>` fragments with `& < > " '` escaped. Override
|
package/dist/index.d.ts
CHANGED
|
@@ -214,6 +214,23 @@ declare const commerceSchema: {
|
|
|
214
214
|
identity: undefined;
|
|
215
215
|
generated: undefined;
|
|
216
216
|
}, {}, {}>;
|
|
217
|
+
publishedAt: drizzle_orm_pg_core.PgColumn<{
|
|
218
|
+
name: "published_at";
|
|
219
|
+
tableName: "commerce_products";
|
|
220
|
+
dataType: "date";
|
|
221
|
+
columnType: "PgTimestamp";
|
|
222
|
+
data: Date;
|
|
223
|
+
driverParam: string;
|
|
224
|
+
notNull: false;
|
|
225
|
+
hasDefault: false;
|
|
226
|
+
isPrimaryKey: false;
|
|
227
|
+
isAutoincrement: false;
|
|
228
|
+
hasRuntimeDefault: false;
|
|
229
|
+
enumValues: undefined;
|
|
230
|
+
baseColumn: never;
|
|
231
|
+
identity: undefined;
|
|
232
|
+
generated: undefined;
|
|
233
|
+
}, {}, {}>;
|
|
217
234
|
createdAt: drizzle_orm_pg_core.PgColumn<{
|
|
218
235
|
name: "created_at";
|
|
219
236
|
tableName: "commerce_products";
|
|
@@ -1899,6 +1916,7 @@ interface OrderEmailLinesHtml {
|
|
|
1899
1916
|
address: string;
|
|
1900
1917
|
}
|
|
1901
1918
|
interface OrderEmailLines {
|
|
1919
|
+
reference: string;
|
|
1902
1920
|
items: EmailLine[];
|
|
1903
1921
|
subtotal: EmailLine;
|
|
1904
1922
|
discount?: EmailLine;
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,7 @@ var products = pgTable(
|
|
|
82
82
|
quantity: integer("quantity").notNull().default(1),
|
|
83
83
|
status: productStatus("status").notNull().default("draft"),
|
|
84
84
|
featured: boolean("featured").notNull().default(false),
|
|
85
|
+
publishedAt: timestamp("published_at", { withTimezone: true }),
|
|
85
86
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
86
87
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
87
88
|
},
|
|
@@ -331,9 +332,6 @@ async function setShippingRates(db, rates) {
|
|
|
331
332
|
return listShippingRates(db);
|
|
332
333
|
}
|
|
333
334
|
|
|
334
|
-
// src/admin/images.ts
|
|
335
|
-
import { Readable } from "stream";
|
|
336
|
-
|
|
337
335
|
// src/admin/repo.ts
|
|
338
336
|
import {
|
|
339
337
|
and as and2,
|
|
@@ -438,14 +436,23 @@ async function setImageAlt(db, id, alt) {
|
|
|
438
436
|
async function selectProductCounts(db) {
|
|
439
437
|
const [row] = await db.select({
|
|
440
438
|
all: sql2`count(*) filter (where ${products.status} <> 'archived')::int`,
|
|
441
|
-
live: sql2`count(*) filter (where ${products.status} = 'active')::int`,
|
|
439
|
+
live: sql2`count(*) filter (where ${products.status} = 'active' and ${products.quantity} > 0)::int`,
|
|
442
440
|
draft: sql2`count(*) filter (where ${products.status} = 'draft')::int`,
|
|
443
|
-
sold: sql2`count(*) filter (where ${products.quantity} = 0)::int`,
|
|
441
|
+
sold: sql2`count(*) filter (where ${products.status} = 'active' and ${products.quantity} = 0)::int`,
|
|
444
442
|
archived: sql2`count(*) filter (where ${products.status} = 'archived')::int`,
|
|
445
443
|
incompleteDrafts: sql2`count(*) filter (where ${products.status} = 'draft' and (${products.name} = '' or ${products.priceCents} is null or ${products.category} is null))::int`
|
|
446
444
|
}).from(products);
|
|
447
445
|
return row;
|
|
448
446
|
}
|
|
447
|
+
async function productHasOrderLines(db, productId) {
|
|
448
|
+
const rows = await db.select({ id: orderItems.id }).from(orderItems).where(eq2(orderItems.productId, productId)).limit(1);
|
|
449
|
+
return rows.length > 0;
|
|
450
|
+
}
|
|
451
|
+
async function deleteProductRows(db, id) {
|
|
452
|
+
await db.delete(inventoryHolds).where(eq2(inventoryHolds.productId, id));
|
|
453
|
+
await db.delete(productImages).where(eq2(productImages.productId, id));
|
|
454
|
+
await db.delete(products).where(eq2(products.id, id));
|
|
455
|
+
}
|
|
449
456
|
|
|
450
457
|
// src/admin/service.ts
|
|
451
458
|
import {
|
|
@@ -481,6 +488,17 @@ var ProductIncompleteError = class extends Error {
|
|
|
481
488
|
missing;
|
|
482
489
|
code = "PRODUCT_INCOMPLETE";
|
|
483
490
|
};
|
|
491
|
+
var ProductNotDeletableError = class extends Error {
|
|
492
|
+
constructor(reason) {
|
|
493
|
+
super(
|
|
494
|
+
reason === "published" ? "This product has been live, so it can only be archived" : "This product is on an order, so it can only be archived"
|
|
495
|
+
);
|
|
496
|
+
this.reason = reason;
|
|
497
|
+
this.name = "ProductNotDeletableError";
|
|
498
|
+
}
|
|
499
|
+
reason;
|
|
500
|
+
code = "PRODUCT_NOT_DELETABLE";
|
|
501
|
+
};
|
|
484
502
|
|
|
485
503
|
// src/admin/publish.ts
|
|
486
504
|
function temporarySlug(id) {
|
|
@@ -529,6 +547,7 @@ function toAdminProduct({ row, images }) {
|
|
|
529
547
|
quantity: row.quantity,
|
|
530
548
|
status: row.status,
|
|
531
549
|
featured: row.featured,
|
|
550
|
+
everPublished: row.publishedAt !== null,
|
|
532
551
|
images: images.map((i) => ({
|
|
533
552
|
id: i.id,
|
|
534
553
|
url: i.url,
|
|
@@ -578,6 +597,7 @@ async function createAdminProduct(db, currency, input, { requirePhotoToPublish =
|
|
|
578
597
|
const slug = uniqueSlug(base, await slugsStartingWith(tx, base));
|
|
579
598
|
return insertProduct(tx, {
|
|
580
599
|
...data,
|
|
600
|
+
publishedAt: data.status === "active" ? /* @__PURE__ */ new Date() : void 0,
|
|
581
601
|
id: newId,
|
|
582
602
|
slug,
|
|
583
603
|
currency: data.currency ?? currency
|
|
@@ -593,6 +613,9 @@ async function patchAdminProduct(db, id, input, { requirePhotoToPublish = true }
|
|
|
593
613
|
assertPublishable(record, fields, photoNeeded);
|
|
594
614
|
}
|
|
595
615
|
const set = sold ? { ...fields, quantity: 0 } : { ...fields };
|
|
616
|
+
if (fields.status === "active" && !record.row.publishedAt) {
|
|
617
|
+
set.publishedAt = /* @__PURE__ */ new Date();
|
|
618
|
+
}
|
|
596
619
|
const renames = fields.name && record.row.slug === temporarySlug(id);
|
|
597
620
|
await db.transaction(async (tx) => {
|
|
598
621
|
if (renames) {
|
|
@@ -607,6 +630,24 @@ async function getAdminProductCounts(db) {
|
|
|
607
630
|
return selectProductCounts(db);
|
|
608
631
|
}
|
|
609
632
|
|
|
633
|
+
// src/admin/delete.ts
|
|
634
|
+
async function deleteAdminProduct(db, storage, id) {
|
|
635
|
+
const { row, images } = await requireProduct(db, id);
|
|
636
|
+
if (row.status !== "draft" || row.publishedAt) {
|
|
637
|
+
throw new ProductNotDeletableError("published");
|
|
638
|
+
}
|
|
639
|
+
if (await productHasOrderLines(db, id)) {
|
|
640
|
+
throw new ProductNotDeletableError("has_orders");
|
|
641
|
+
}
|
|
642
|
+
await db.transaction((tx) => deleteProductRows(tx, id));
|
|
643
|
+
for (const image of images) {
|
|
644
|
+
if (image.storageKey) await storage.delete(image.storageKey);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/admin/images.ts
|
|
649
|
+
import { Readable } from "stream";
|
|
650
|
+
|
|
610
651
|
// src/admin/upload.ts
|
|
611
652
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
612
653
|
import { fileTypeFromBuffer } from "file-type";
|
|
@@ -712,6 +753,13 @@ var adminPlugin = async (app, { db, currency, authorize, storage, requirePhotoTo
|
|
|
712
753
|
missing: err.missing
|
|
713
754
|
});
|
|
714
755
|
}
|
|
756
|
+
if (err instanceof ProductNotDeletableError) {
|
|
757
|
+
return reply.code(409).send({
|
|
758
|
+
error: err.code,
|
|
759
|
+
message: err.message,
|
|
760
|
+
reason: err.reason
|
|
761
|
+
});
|
|
762
|
+
}
|
|
715
763
|
if (err instanceof InvalidCursorError || err instanceof InvalidImageError || err instanceof InvalidImageOrderError) {
|
|
716
764
|
return reply.code(400).send({ error: err.code, message: err.message });
|
|
717
765
|
}
|
|
@@ -752,6 +800,13 @@ var adminPlugin = async (app, { db, currency, authorize, storage, requirePhotoTo
|
|
|
752
800
|
{ requirePhotoToPublish }
|
|
753
801
|
)
|
|
754
802
|
);
|
|
803
|
+
app.delete(
|
|
804
|
+
"/products/:id",
|
|
805
|
+
async (req, reply) => {
|
|
806
|
+
await deleteAdminProduct(db, storage, req.params.id);
|
|
807
|
+
return reply.code(204).send();
|
|
808
|
+
}
|
|
809
|
+
);
|
|
755
810
|
app.post(
|
|
756
811
|
"/products/:id/images",
|
|
757
812
|
async (req, reply) => {
|
|
@@ -1217,6 +1272,9 @@ import {
|
|
|
1217
1272
|
shipOrderInputSchema
|
|
1218
1273
|
} from "@develemit/commerce-contracts";
|
|
1219
1274
|
|
|
1275
|
+
// src/orders/admin-service.ts
|
|
1276
|
+
import { orderRefSearchTerm } from "@develemit/commerce-contracts";
|
|
1277
|
+
|
|
1220
1278
|
// src/orders/admin-repo.ts
|
|
1221
1279
|
import { and as and5, desc as desc2, eq as eq5, ilike as ilike2, lt as lt3, or as or3, sql as sql5 } from "drizzle-orm";
|
|
1222
1280
|
function escapeLike2(value) {
|
|
@@ -1488,7 +1546,7 @@ function decodeOrderCursor(raw) {
|
|
|
1488
1546
|
async function listAdminOrders(db, query) {
|
|
1489
1547
|
const rows = await selectOrderPage(db, {
|
|
1490
1548
|
status: query.status,
|
|
1491
|
-
q: query.q,
|
|
1549
|
+
q: query.q ? orderRefSearchTerm(query.q) || void 0 : void 0,
|
|
1492
1550
|
hasPendingRefund: query.hasPendingRefund,
|
|
1493
1551
|
after: query.cursor ? decodeOrderCursor(query.cursor) : void 0,
|
|
1494
1552
|
limit: query.limit + 1
|
|
@@ -3192,6 +3250,9 @@ function startHoldSweeper(db, { intervalMs = 6e4, onError } = {}) {
|
|
|
3192
3250
|
}
|
|
3193
3251
|
|
|
3194
3252
|
// src/emails/order-lines.ts
|
|
3253
|
+
import {
|
|
3254
|
+
formatOrderRef
|
|
3255
|
+
} from "@develemit/commerce-contracts";
|
|
3195
3256
|
var defaultLabels = {
|
|
3196
3257
|
subtotal: "Subtotal",
|
|
3197
3258
|
discount: (code) => code ? `Discount (${code})` : "Discount",
|
|
@@ -3255,6 +3316,7 @@ function orderEmailLines(order, options = {}) {
|
|
|
3255
3316
|
const address = addressLines(order);
|
|
3256
3317
|
const totals = [subtotal, ...discount ? [discount] : [], shipping, total];
|
|
3257
3318
|
return {
|
|
3319
|
+
reference: formatOrderRef(order.id),
|
|
3258
3320
|
items,
|
|
3259
3321
|
subtotal,
|
|
3260
3322
|
discount,
|