@stacksjs/commerce 0.75.348 → 0.75.349
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.
|
@@ -25,4 +25,4 @@ declare type NewPayment = NewModelData<typeof Payment>;
|
|
|
25
25
|
* caller can surface a specific error to the user.
|
|
26
26
|
*/
|
|
27
27
|
export type PlaceOrderResult = | { ok: true, order: OrderJsonResponse, items?: OrderItemJsonResponse[], payment?: PaymentJsonResponse }
|
|
28
|
-
| { ok: false, reason: 'out-of-stock' | 'duplicate-payment' | 'duplicate-idempotency-key' | 'unknown', failedAt?: string, error?: unknown }
|
|
28
|
+
| { ok: false, reason: 'out-of-stock' | 'duplicate-order' | 'duplicate-payment' | 'duplicate-idempotency-key' | 'unknown', failedAt?: string, error?: unknown }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../utils/model-row";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{emitOrderCreated,emitOrderPaid}from"./events";import{adjustInventoryOnConnection}from"../utils/inventory-adjustment";let warnedAboutMissingOrderIdempotencyTable=!1;function isMissingTableError(err){const e=err,msg=e?.message??"";return e?.code==="42P01"||msg.includes("no such table")||msg.includes("doesn't exist")||/relation "[^"]*" does not exist/i.test(msg)}export async function findOrderByIdempotencyKey(key){try{const row=await db.selectFrom("order_idempotency").where("idempotency_key","=",key).selectAll().executeTakeFirst();if(!row)return null;return await db.selectFrom("orders").where("id","=",row.order_id).selectAll().executeTakeFirst()??null}catch(err){if(isMissingTableError(err)){if(!warnedAboutMissingOrderIdempotencyTable){warnedAboutMissingOrderIdempotencyTable=!0;console.warn("[commerce/orders] order_idempotency table missing - idempotency keys are accepted but NOT enforced. Run migrations to enable dedup.")}return null}throw err}}export async function placeOrder(input){if(input.idempotencyKey){const existing=await findOrderByIdempotencyKey(input.idempotencyKey);if(existing)return{ok:!0,order:existing}}try{const result=await db.transaction(async(trx)=>{const now=formatDate(new Date),orderUuid=randomUUIDv7(),orderData={...input.order,status:input.order.status||"PENDING",uuid:orderUuid,created_at:now,updated_at:now};await trx.insertInto("orders").values(orderData).executeTakeFirst();const insertedOrder=asModelRow(await trx.selectFrom("orders").where("uuid","=",orderUuid).selectAll().executeTakeFirst(),!0),orderId=Number(insertedOrder?.id);if(!Number.isSafeInteger(orderId)||orderId<=0)throw Object.assign(Error("order insert could not be resolved by uuid"),{__placeFail:!0,failedAt:"order",reason:"unknown"});let orderItemRows;if(input.items&&input.items.length>0){const values=input.items.map((item)=>{if(!Number.isInteger(item.productId)||item.productId<=0)throw Object.assign(Error("order item product id must be a positive integer"),{__placeFail:!0,failedAt:"items",reason:"unknown"});if(!Number.isInteger(item.quantity)||item.quantity<=0)throw Object.assign(Error("order item quantity must be a positive integer"),{__placeFail:!0,failedAt:"items",reason:"unknown"});if(!Number.isFinite(item.price)||item.price<0)throw Object.assign(Error("order item price must be a non-negative finite number"),{__placeFail:!0,failedAt:"items",reason:"unknown"});return{order_id:orderId,product_id:item.productId,quantity:item.quantity,price:item.price,special_instructions:item.specialInstructions||null,created_at:now,updated_at:now}});await trx.insertInto("order_items").values(values).execute();orderItemRows=asModelRows(await trx.selectFrom("order_items").where("order_id","=",orderId).selectAll().execute())}let paymentRow;if(input.payment)try{const paymentUuid=randomUUIDv7();await trx.insertInto("payments").values({...input.payment,order_id:orderId,uuid:paymentUuid,created_at:now,updated_at:now}).executeTakeFirst();paymentRow=asModelRow(await trx.selectFrom("payments").where("uuid","=",paymentUuid).selectAll().executeTakeFirst(),!0);if(!paymentRow)throw Object.assign(Error("payment insert could not be resolved by uuid"),{__placeFail:!0,failedAt:"payment",reason:"unknown"})}catch(err){if(isUniqueViolation(err))throw Object.assign(Error("duplicate payment transaction_id"),{__placeFail:!0,failedAt:"payment",reason:"duplicate-payment"});throw Object.assign(err,{__placeFail:!0,failedAt:"payment",reason:"unknown"})}if(input.inventory&&input.inventory.length>0)for(let i=0;i<input.inventory.length;i++){const{id,delta}=input.inventory[i];if(!Number.isFinite(delta)||delta===0)throw Object.assign(Error("inventory delta must be a non-zero finite number"),{__placeFail:!0,failedAt:"inventory",reason:"unknown"});if(!await adjustInventoryOnConnection(trx,id,delta,now))throw Object.assign(Error(`inventory adjust failed for product ${id}`),{__placeFail:!0,failedAt:"inventory",reason:"out-of-stock"})}if(input.idempotencyKey)try{await trx.insertInto("order_idempotency").values({idempotency_key:input.idempotencyKey,order_id:orderId,created_at:now}).execute()}catch(err){if(isUniqueViolation(err))throw Object.assign(Error("idempotency-key collision"),{__placeFail:!0,failedAt:"idempotency",reason:"duplicate-idempotency-key"});if(!isMissingTableError(err))throw err}const order=asModelRow(await trx.selectFrom("orders").where("id","=",orderId).selectAll().executeTakeFirst(),!0);if(!order)throw Object.assign(Error("order disappeared mid-transaction"),{__placeFail:!0,failedAt:"order",reason:"unknown"});return{order,items:orderItemRows,payment:paymentRow}});emitOrderCreated(result.order);if(result.payment)emitOrderPaid(result.order,result.payment);return{ok:!0,order:result.order,items:result.items,payment:result.payment}}catch(err){if(err?.__placeFail){if(err.reason==="duplicate-idempotency-key"&&input.idempotencyKey){const existing=await findOrderByIdempotencyKey(input.idempotencyKey);if(existing)return{ok:!0,order:existing}}return{ok:!1,reason:err.reason,failedAt:err.failedAt,error:err}}throw err}}
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../utils/model-row";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{emitOrderCreated,emitOrderPaid}from"./events";import{adjustInventoryOnConnection}from"../utils/inventory-adjustment";let warnedAboutMissingOrderIdempotencyTable=!1;function isMissingTableError(err){const e=err,msg=e?.message??"";return e?.code==="42P01"||msg.includes("no such table")||msg.includes("doesn't exist")||/relation "[^"]*" does not exist/i.test(msg)}export async function findOrderByIdempotencyKey(key){try{const row=await db.selectFrom("order_idempotency").where("idempotency_key","=",key).selectAll().executeTakeFirst();if(!row)return null;return await db.selectFrom("orders").where("id","=",row.order_id).selectAll().executeTakeFirst()??null}catch(err){if(isMissingTableError(err)){if(!warnedAboutMissingOrderIdempotencyTable){warnedAboutMissingOrderIdempotencyTable=!0;console.warn("[commerce/orders] order_idempotency table missing - idempotency keys are accepted but NOT enforced. Run migrations to enable dedup.")}return null}throw err}}export async function placeOrder(input){if(input.idempotencyKey){const existing=await findOrderByIdempotencyKey(input.idempotencyKey);if(existing)return{ok:!0,order:existing}}try{const result=await db.transaction(async(trx)=>{const now=formatDate(new Date),orderUuid=randomUUIDv7(),orderData={...input.order,status:input.order.status||"PENDING",uuid:orderUuid,created_at:now,updated_at:now};try{await trx.insertInto("orders").values(orderData).executeTakeFirst()}catch(err){if(isUniqueViolation(err))throw Object.assign(Error("duplicate order identity",{cause:err}),{__placeFail:!0,failedAt:"order",reason:"duplicate-order"});throw err}const insertedOrder=asModelRow(await trx.selectFrom("orders").where("uuid","=",orderUuid).selectAll().executeTakeFirst(),!0),orderId=Number(insertedOrder?.id);if(!Number.isSafeInteger(orderId)||orderId<=0)throw Object.assign(Error("order insert could not be resolved by uuid"),{__placeFail:!0,failedAt:"order",reason:"unknown"});let orderItemRows;if(input.items&&input.items.length>0){const values=input.items.map((item)=>{if(!Number.isInteger(item.productId)||item.productId<=0)throw Object.assign(Error("order item product id must be a positive integer"),{__placeFail:!0,failedAt:"items",reason:"unknown"});if(!Number.isInteger(item.quantity)||item.quantity<=0)throw Object.assign(Error("order item quantity must be a positive integer"),{__placeFail:!0,failedAt:"items",reason:"unknown"});if(!Number.isFinite(item.price)||item.price<0)throw Object.assign(Error("order item price must be a non-negative finite number"),{__placeFail:!0,failedAt:"items",reason:"unknown"});return{order_id:orderId,product_id:item.productId,quantity:item.quantity,price:item.price,special_instructions:item.specialInstructions||null,created_at:now,updated_at:now}});await trx.insertInto("order_items").values(values).execute();orderItemRows=asModelRows(await trx.selectFrom("order_items").where("order_id","=",orderId).selectAll().execute())}let paymentRow;if(input.payment)try{const paymentUuid=randomUUIDv7();await trx.insertInto("payments").values({...input.payment,order_id:orderId,uuid:paymentUuid,created_at:now,updated_at:now}).executeTakeFirst();paymentRow=asModelRow(await trx.selectFrom("payments").where("uuid","=",paymentUuid).selectAll().executeTakeFirst(),!0);if(!paymentRow)throw Object.assign(Error("payment insert could not be resolved by uuid"),{__placeFail:!0,failedAt:"payment",reason:"unknown"})}catch(err){if(isUniqueViolation(err))throw Object.assign(Error("duplicate payment transaction_id"),{__placeFail:!0,failedAt:"payment",reason:"duplicate-payment"});throw Object.assign(err,{__placeFail:!0,failedAt:"payment",reason:"unknown"})}if(input.inventory&&input.inventory.length>0)for(let i=0;i<input.inventory.length;i++){const{id,delta}=input.inventory[i];if(!Number.isFinite(delta)||delta===0)throw Object.assign(Error("inventory delta must be a non-zero finite number"),{__placeFail:!0,failedAt:"inventory",reason:"unknown"});if(!await adjustInventoryOnConnection(trx,id,delta,now))throw Object.assign(Error(`inventory adjust failed for product ${id}`),{__placeFail:!0,failedAt:"inventory",reason:"out-of-stock"})}if(input.idempotencyKey)try{await trx.insertInto("order_idempotency").values({idempotency_key:input.idempotencyKey,order_id:orderId,created_at:now}).execute()}catch(err){if(isUniqueViolation(err))throw Object.assign(Error("idempotency-key collision"),{__placeFail:!0,failedAt:"idempotency",reason:"duplicate-idempotency-key"});if(!isMissingTableError(err))throw err}const order=asModelRow(await trx.selectFrom("orders").where("id","=",orderId).selectAll().executeTakeFirst(),!0);if(!order)throw Object.assign(Error("order disappeared mid-transaction"),{__placeFail:!0,failedAt:"order",reason:"unknown"});return{order,items:orderItemRows,payment:paymentRow}});emitOrderCreated(result.order);if(result.payment)emitOrderPaid(result.order,result.payment);return{ok:!0,order:result.order,items:result.items,payment:result.payment}}catch(err){if(err?.__placeFail){if(err.reason==="duplicate-idempotency-key"&&input.idempotencyKey){const existing=await findOrderByIdempotencyKey(input.idempotencyKey);if(existing)return{ok:!0,order:existing}}return{ok:!1,reason:err.reason,failedAt:err.failedAt,error:err}}throw err}}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/commerce",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.75.
|
|
4
|
+
"version": "0.75.349",
|
|
5
5
|
"description": "Stacks commerce utilities.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -57,19 +57,19 @@
|
|
|
57
57
|
"prepublishOnly": "bun run build"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@stacksjs/cache": "0.75.
|
|
61
|
-
"@stacksjs/config": "0.75.
|
|
62
|
-
"@stacksjs/database": "0.75.
|
|
63
|
-
"@stacksjs/env": "0.75.
|
|
64
|
-
"@stacksjs/error-handling": "0.75.
|
|
65
|
-
"@stacksjs/events": "0.75.
|
|
66
|
-
"@stacksjs/logging": "0.75.
|
|
67
|
-
"@stacksjs/orm": "0.75.
|
|
68
|
-
"@stacksjs/payments": "0.75.
|
|
69
|
-
"@stacksjs/realtime": "0.75.
|
|
70
|
-
"@stacksjs/slug": "0.75.
|
|
71
|
-
"@stacksjs/strings": "0.75.
|
|
72
|
-
"@stacksjs/validation": "0.75.
|
|
60
|
+
"@stacksjs/cache": "0.75.349",
|
|
61
|
+
"@stacksjs/config": "0.75.349",
|
|
62
|
+
"@stacksjs/database": "0.75.349",
|
|
63
|
+
"@stacksjs/env": "0.75.349",
|
|
64
|
+
"@stacksjs/error-handling": "0.75.349",
|
|
65
|
+
"@stacksjs/events": "0.75.349",
|
|
66
|
+
"@stacksjs/logging": "0.75.349",
|
|
67
|
+
"@stacksjs/orm": "0.75.349",
|
|
68
|
+
"@stacksjs/payments": "0.75.349",
|
|
69
|
+
"@stacksjs/realtime": "0.75.349",
|
|
70
|
+
"@stacksjs/slug": "0.75.349",
|
|
71
|
+
"@stacksjs/strings": "0.75.349",
|
|
72
|
+
"@stacksjs/validation": "0.75.349",
|
|
73
73
|
"ts-spreadsheets": "^0.2.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|