@stacksjs/commerce 0.75.112 → 0.75.113
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/README.md +15 -0
- package/dist/auctions/bids/fetch.js +1 -0
- package/dist/auctions/bids/index.js +1 -0
- package/dist/auctions/bids/place.js +1 -0
- package/dist/auctions/engine/anti-snipe.js +1 -0
- package/dist/auctions/engine/increments.js +1 -0
- package/dist/auctions/engine/index.js +1 -0
- package/dist/auctions/engine/proxy.js +1 -0
- package/dist/auctions/engine/winners.js +1 -0
- package/dist/auctions/index.js +1 -0
- package/dist/auctions/lots/fetch.js +1 -0
- package/dist/auctions/lots/index.js +1 -0
- package/dist/auctions/lots/lifecycle.js +1 -0
- package/dist/auctions/notifications.js +1 -0
- package/dist/auctions/pledges/index.js +1 -0
- package/dist/auctions/realtime.js +1 -0
- package/dist/auctions/rules.js +1 -0
- package/dist/auctions/types.js +0 -0
- package/dist/carts/destroy.js +1 -0
- package/dist/carts/fetch.js +1 -0
- package/dist/carts/index.js +1 -0
- package/dist/carts/store.js +1 -0
- package/dist/carts/update.js +1 -0
- package/dist/catalog.js +1 -2
- package/dist/coupons/destroy.js +1 -0
- package/dist/coupons/fetch.js +1 -0
- package/dist/coupons/index.js +1 -0
- package/dist/coupons/store.js +1 -0
- package/dist/coupons/update.js +7 -0
- package/dist/customers/destroy.js +1 -0
- package/dist/customers/fetch.js +1 -0
- package/dist/customers/index.js +1 -0
- package/dist/customers/store.js +1 -0
- package/dist/customers/update.js +1 -0
- package/dist/devices/destroy.js +1 -0
- package/dist/devices/export.js +1 -0
- package/dist/devices/fetch.js +1 -0
- package/dist/devices/index.js +1 -0
- package/dist/devices/store.js +1 -0
- package/dist/devices/update.js +1 -0
- package/dist/errors/destroy.js +1 -0
- package/dist/errors/fetch.js +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/errors/update.js +1 -0
- package/dist/gift-cards/destroy.js +1 -0
- package/dist/gift-cards/fetch.js +1 -0
- package/dist/gift-cards/index.js +1 -0
- package/dist/gift-cards/store.js +1 -0
- package/dist/gift-cards/update.js +10 -0
- package/dist/imports/account-mapping.js +1 -0
- package/dist/imports/account-types.js +0 -0
- package/dist/imports/account-writer.js +1 -0
- package/dist/imports/admin-http.js +1 -0
- package/dist/imports/credentials.js +1 -0
- package/dist/imports/http.js +1 -0
- package/dist/imports/identity.js +1 -0
- package/dist/imports/index.js +1 -0
- package/dist/imports/money.js +1 -0
- package/dist/imports/shopify-admin.js +1 -0
- package/dist/imports/shopify.js +1 -0
- package/dist/imports/shopware-admin.js +1 -0
- package/dist/imports/shopware.js +1 -0
- package/dist/imports/text.js +1 -0
- package/dist/imports/types.js +0 -0
- package/dist/imports/woocommerce-admin.js +1 -0
- package/dist/imports/woocommerce.js +1 -0
- package/dist/imports/writer.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -28
- package/dist/inventory.d.ts +2 -0
- package/dist/inventory.js +1 -0
- package/dist/money.js +1 -2
- package/dist/orders/destroy.js +1 -0
- package/dist/orders/events.js +1 -0
- package/dist/orders/export.js +1 -0
- package/dist/orders/fetch.js +1 -0
- package/dist/orders/guards.js +1 -0
- package/dist/orders/index.js +1 -0
- package/dist/orders/place-order.js +1 -0
- package/dist/orders/store.js +1 -0
- package/dist/orders/totals.js +1 -0
- package/dist/orders/transition.js +1 -0
- package/dist/orders/update.js +1 -0
- package/dist/orders/webhook.js +1 -0
- package/dist/payments/destroy.js +1 -0
- package/dist/payments/fetch.js +1 -0
- package/dist/payments/index.js +1 -0
- package/dist/payments/store.js +1 -0
- package/dist/payments/update.js +8 -0
- package/dist/product.d.ts +1 -0
- package/dist/product.js +0 -0
- package/dist/products/categories/destroy.js +1 -0
- package/dist/products/categories/fetch.js +1 -0
- package/dist/products/categories/index.js +1 -0
- package/dist/products/categories/store.js +1 -0
- package/dist/products/categories/types.js +0 -0
- package/dist/products/categories/update.js +1 -0
- package/dist/products/index.js +1 -0
- package/dist/products/items/destroy.js +1 -0
- package/dist/products/items/fetch.js +1 -0
- package/dist/products/items/index.d.ts +1 -0
- package/dist/products/items/index.js +1 -0
- package/dist/products/items/store.js +1 -0
- package/dist/products/items/update.d.ts +2 -0
- package/dist/products/items/update.js +1 -0
- package/dist/products/manufacturers/destroy.js +1 -0
- package/dist/products/manufacturers/fetch.js +1 -0
- package/dist/products/manufacturers/index.js +1 -0
- package/dist/products/manufacturers/store.js +1 -0
- package/dist/products/manufacturers/update.js +1 -0
- package/dist/products/reviews/destroy.js +1 -0
- package/dist/products/reviews/fetch.js +1 -0
- package/dist/products/reviews/index.js +1 -0
- package/dist/products/reviews/stats.js +1 -0
- package/dist/products/reviews/store.js +1 -0
- package/dist/products/reviews/update.js +1 -0
- package/dist/products/units/destroy.js +1 -0
- package/dist/products/units/fetch.js +1 -0
- package/dist/products/units/index.js +1 -0
- package/dist/products/units/store.js +1 -0
- package/dist/products/units/types.js +0 -0
- package/dist/products/units/update.js +1 -0
- package/dist/products/variants/destroy.js +1 -0
- package/dist/products/variants/fetch.js +1 -0
- package/dist/products/variants/index.js +1 -0
- package/dist/products/variants/store.js +1 -0
- package/dist/products/variants/update.js +1 -0
- package/dist/receipts/destroy.js +1 -0
- package/dist/receipts/export.d.ts +1 -0
- package/dist/receipts/export.js +0 -0
- package/dist/receipts/fetch.js +1 -0
- package/dist/receipts/index.js +1 -0
- package/dist/receipts/printer.d.ts +37 -0
- package/dist/receipts/printer.js +1 -0
- package/dist/receipts/store.js +1 -0
- package/dist/receipts/timestamp.js +1 -0
- package/dist/receipts/update.js +1 -0
- package/dist/refund-allocation.js +1 -2
- package/dist/register.js +1 -2
- package/dist/releases.js +1 -2
- package/dist/sales-tax.js +1 -2
- package/dist/shippings/couriers/destroy.js +1 -0
- package/dist/shippings/couriers/fetch.js +1 -0
- package/dist/shippings/couriers/index.js +1 -0
- package/dist/shippings/couriers/store.js +1 -0
- package/dist/shippings/couriers/update.js +1 -0
- package/dist/shippings/delivery-routes/destroy.js +1 -0
- package/dist/shippings/delivery-routes/fetch.js +1 -0
- package/dist/shippings/delivery-routes/index.js +1 -0
- package/dist/shippings/delivery-routes/store.js +1 -0
- package/dist/shippings/delivery-routes/update.js +1 -0
- package/dist/shippings/delivery-routes/validate-write.js +1 -0
- package/dist/shippings/digital-deliveries/destroy.js +1 -0
- package/dist/shippings/digital-deliveries/fetch.js +1 -0
- package/dist/shippings/digital-deliveries/index.js +1 -0
- package/dist/shippings/digital-deliveries/store.js +1 -0
- package/dist/shippings/digital-deliveries/update.js +1 -0
- package/dist/shippings/geocoding/index.js +1 -0
- package/dist/shippings/geocoding/nominatim.js +1 -0
- package/dist/shippings/geocoding/types.js +1 -0
- package/dist/shippings/index.js +1 -0
- package/dist/shippings/license-keys/destroy.js +1 -0
- package/dist/shippings/license-keys/fetch.js +1 -0
- package/dist/shippings/license-keys/index.js +1 -0
- package/dist/shippings/license-keys/store.js +1 -0
- package/dist/shippings/license-keys/update.js +1 -0
- package/dist/shippings/license-keys/validate-write.js +1 -0
- package/dist/shippings/shipping-methods/destroy.js +1 -0
- package/dist/shippings/shipping-methods/fetch.js +1 -0
- package/dist/shippings/shipping-methods/index.js +1 -0
- package/dist/shippings/shipping-methods/store.js +1 -0
- package/dist/shippings/shipping-methods/update.js +1 -0
- package/dist/shippings/shipping-rates/destroy.js +1 -0
- package/dist/shippings/shipping-rates/fetch.js +1 -0
- package/dist/shippings/shipping-rates/index.js +1 -0
- package/dist/shippings/shipping-rates/store.js +1 -0
- package/dist/shippings/shipping-rates/update.js +1 -0
- package/dist/shippings/shipping-rates/validate-write.js +1 -0
- package/dist/shippings/shipping-zones/destroy.js +1 -0
- package/dist/shippings/shipping-zones/fetch.js +1 -0
- package/dist/shippings/shipping-zones/index.js +1 -0
- package/dist/shippings/shipping-zones/store.js +1 -0
- package/dist/shippings/shipping-zones/update.js +1 -0
- package/dist/shippings/tracking/events.js +1 -0
- package/dist/shippings/tracking/geo.js +1 -0
- package/dist/shippings/tracking/index.js +1 -0
- package/dist/shippings/tracking/ping.js +1 -0
- package/dist/shippings/tracking/stops.js +1 -0
- package/dist/shippings/write-data.js +1 -0
- package/dist/tax/breakdown.js +1 -0
- package/dist/tax/destroy.js +1 -0
- package/dist/tax/fetch.js +1 -0
- package/dist/tax/index.js +1 -0
- package/dist/tax/store.js +1 -0
- package/dist/tax/types.js +0 -0
- package/dist/tax/update.js +1 -0
- package/dist/tax-input.d.ts +7 -0
- package/dist/tax-input.js +1 -0
- package/dist/tests/fixtures/returning-all-writes.d.ts +1 -0
- package/dist/tests/fixtures/returning-all-writes.js +1 -0
- package/dist/tests/fixtures/soft-delete-counts.d.ts +1 -0
- package/dist/tests/fixtures/soft-delete-counts.js +1 -0
- package/dist/tests/fixtures/write-counts.d.ts +1 -0
- package/dist/tests/fixtures/write-counts.js +8 -0
- package/dist/tests/setup.d.ts +5 -0
- package/dist/tests/setup.js +1 -0
- package/dist/types.js +0 -0
- package/dist/utils/inserted-id.js +1 -0
- package/dist/utils/inventory-adjustment.js +5 -0
- package/dist/utils/model-row.js +1 -0
- package/dist/utils/mutation-count.js +1 -0
- package/dist/utils/typed-stats.js +1 -0
- package/dist/waitlists/index.js +1 -0
- package/dist/waitlists/products/destroy.js +1 -0
- package/dist/waitlists/products/export.d.ts +41 -0
- package/dist/waitlists/products/export.js +1 -0
- package/dist/waitlists/products/fetch.js +1 -0
- package/dist/waitlists/products/index.js +1 -0
- package/dist/waitlists/products/store.js +1 -0
- package/dist/waitlists/products/update.js +1 -0
- package/dist/waitlists/products/write-data.js +1 -0
- package/dist/waitlists/restaurant/destroy.js +1 -0
- package/dist/waitlists/restaurant/export.d.ts +39 -0
- package/dist/waitlists/restaurant/export.js +1 -0
- package/dist/waitlists/restaurant/fetch.js +1 -0
- package/dist/waitlists/restaurant/index.js +1 -0
- package/dist/waitlists/restaurant/store.js +1 -0
- package/dist/waitlists/restaurant/update.js +1 -0
- package/dist/waitlists/restaurant/write-data.js +1 -0
- package/package.json +13 -12
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{InputValidationError,parseNumberInput}from"@stacksjs/validation/input";export function restockedQuantity(stock,units,maximum=2147483647){const current=parseNumberInput(stock,"Inventory",0,maximum,!0),incoming=parseNumberInput(units,"Restock",1,maximum,!0);if(current===null)throw new InputValidationError("Set inventory before restocking an untracked product");if(incoming===null)throw new InputValidationError("Restock is required");const quantity=current+incoming;if(!Number.isSafeInteger(quantity)||quantity>maximum)throw new InputValidationError(`Restocked inventory must not exceed ${maximum} units`);return quantity}
|
package/dist/money.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
var p=Object.defineProperty;var b=(n)=>n;function h(n,e){this[n]=b.bind(null,e)}var x=(n,e)=>{for(var t in e)p(n,t,{get:e[t],enumerable:!0,configurable:!0,set:h.bind(e,t)})};var O=(n,e,t)=>()=>{if(n)try{e=n(n=0)}catch(r){t=[r]}if(t)throw t[0];return e};var S={};x(S,{PriceFormatError:()=>l,currencyExponent:()=>s,decimalToMinor:()=>m,formatCurrency:()=>y,formatMinor:()=>c,minorToDecimal:()=>a,minorToInput:()=>M,minorToMajor:()=>f,moneyInputError:()=>g,moneyInputStep:()=>I,parseMoneyInput:()=>N,rescaleMinor:()=>w});class l extends Error{value;constructor(n,e){super(`Unreadable price ${JSON.stringify(n)}: ${e}`);this.value=n;this.name="PriceFormatError"}}function s(n){if(!n)return 2;try{let e=new Intl.NumberFormat("en",{style:"currency",currency:n}).resolvedOptions().maximumFractionDigits;return typeof e==="number"?e:2}catch{return 2}}function m(n,e=2){if(n===null||n===void 0)return null;let t=String(n).trim();if(t==="")return null;let r=/^(\d*)(?:\.(\d*))?$/.exec(t);if(!r||!/\d/.test(t))throw new l(n,"expected a plain decimal such as 19.99");let u=r[1]||"0",i=r[2]??"",o;if(i.length<=e)o=BigInt(u+i.padEnd(e,"0"));else if(o=BigInt(u+i.slice(0,e)),i.charCodeAt(e)>=53)o+=1n;if(o>BigInt(Number.MAX_SAFE_INTEGER))throw new l(n,"too large to store exactly");return Number(o)}function a(n,e=2){let t=String(n).trim();if(!/^\d+$/.test(t))throw new l(n,"expected a non-negative integer amount in minor units");if(e===0)return t.replace(/^0+(?=\d)/,"");let r=t.padStart(e+1,"0");return`${r.slice(0,-e).replace(/^0+(?=\d)/,"")}.${r.slice(-e)}`}function w(n,e,t){if(n===null||n===void 0||String(n).trim()==="")return null;if(!Number.isInteger(e)||e<0)throw new l(n,`invalid source precision ${e}`);return m(a(String(n),e),t)}function c(n,e){let t=s(e),r=Math.round(Number(n)),u=Number.isSafeInteger(r)?`${r<0?"-":""}${a(Math.abs(r),t)}`:String(n);return e?`${u} ${e}`:u}function f(n,e){let t=Math.round(Number(n??0));if(!Number.isFinite(t))return 0;let r=a(Math.abs(t),s(e));return t<0?-Number(r):Number(r)}function y(n,e,t){let r=(e||"USD").toUpperCase(),u=f(n,r);try{return new Intl.NumberFormat(t,{style:"currency",currency:r}).format(u)}catch{return c(Math.round(Number(n??0))||0,r)}}function M(n,e){if(n===null||n===void 0||n==="")return"";let t=Math.round(Number(n));if(!Number.isFinite(t))return"";let r=a(Math.abs(t),s(e));return t<0?`-${r}`:r}function I(n){let e=s(n);return e===0?"1":`0.${"1".padStart(e,"0")}`}function g(n,e,t={}){let r=n===null||n===void 0?"":String(n).trim(),u=s(e),i=(e||"USD").toUpperCase();if(r==="")return t.required===!1?"":"Enter an amount.";let o=/^(\d*)(?:\.(\d*))?$/.exec(r);if(!o||!/\d/.test(r))return u===0?"Enter a whole number, for example 1999.":`Enter a plain amount, for example ${a(1999,u)}.`;if((o[2]??"").length>u)return u===0?`${i} amounts have no decimal places.`:`${i} amounts have at most ${u} decimal ${u===1?"place":"places"}.`;let d;try{d=m(r,u)}catch{return"That amount is too large."}if(d===null)return"Enter an amount.";if(t.min!==void 0&&d<t.min)return`Enter at least ${c(t.min,i)}.`;if(t.max!==void 0&&d>t.max)return`Enter at most ${c(t.max,i)}.`;return""}function N(n,e,t={}){let r=g(n,e,t);if(r)throw new l(n,r);return m(n,s(e))}export{l as PriceFormatError,s as currencyExponent,m as decimalToMinor,y as formatCurrency,c as formatMinor,a as minorToDecimal,M as minorToInput,f as minorToMajor,g as moneyInputError,I as moneyInputStep,N as parseMoneyInput,w as rescaleMinor};
|
|
1
|
+
export class PriceFormatError extends Error{value;constructor(value,reason){super(`Unreadable price ${JSON.stringify(value)}: ${reason}`);this.value=value;this.name="PriceFormatError"}}export function currencyExponent(currency){if(!currency)return 2;try{const digits=new Intl.NumberFormat("en",{style:"currency",currency}).resolvedOptions().maximumFractionDigits;return typeof digits==="number"?digits:2}catch{return 2}}export function decimalToMinor(value,exponent=2){if(value===null||value===void 0)return null;const raw=String(value).trim();if(raw==="")return null;const match=/^(\d*)(?:\.(\d*))?$/.exec(raw);if(!match||!/\d/.test(raw))throw new PriceFormatError(value,"expected a plain decimal such as 19.99");const whole=match[1]||"0",fraction=match[2]??"";let minor;if(fraction.length<=exponent)minor=BigInt(whole+fraction.padEnd(exponent,"0"));else{minor=BigInt(whole+fraction.slice(0,exponent));if(fraction.charCodeAt(exponent)>=53)minor+=1n}if(minor>BigInt(Number.MAX_SAFE_INTEGER))throw new PriceFormatError(value,"too large to store exactly");return Number(minor)}export function minorToDecimal(minor,exponent=2){const raw=String(minor).trim();if(!/^\d+$/.test(raw))throw new PriceFormatError(minor,"expected a non-negative integer amount in minor units");if(exponent===0)return raw.replace(/^0+(?=\d)/,"");const padded=raw.padStart(exponent+1,"0");return`${padded.slice(0,-exponent).replace(/^0+(?=\d)/,"")}.${padded.slice(-exponent)}`}export function rescaleMinor(amount,fromExponent,toExponent){if(amount===null||amount===void 0||String(amount).trim()==="")return null;if(!Number.isInteger(fromExponent)||fromExponent<0)throw new PriceFormatError(amount,`invalid source precision ${fromExponent}`);return decimalToMinor(minorToDecimal(String(amount),fromExponent),toExponent)}export function formatMinor(minor,currency){const exponent=currencyExponent(currency),whole=Math.round(Number(minor)),amount=Number.isSafeInteger(whole)?`${whole<0?"-":""}${minorToDecimal(Math.abs(whole),exponent)}`:String(minor);return currency?`${amount} ${currency}`:amount}export function minorToMajor(minor,currency){const value=Math.round(Number(minor??0));if(!Number.isFinite(value))return 0;const decimal=minorToDecimal(Math.abs(value),currencyExponent(currency));return value<0?-Number(decimal):Number(decimal)}export function formatCurrency(minor,currency,locale){const code=(currency||"USD").toUpperCase(),major=minorToMajor(minor,code);try{return new Intl.NumberFormat(locale,{style:"currency",currency:code}).format(major)}catch{return formatMinor(Math.round(Number(minor??0))||0,code)}}export function minorToInput(minor,currency){if(minor===null||minor===void 0||minor==="")return"";const value=Math.round(Number(minor));if(!Number.isFinite(value))return"";const decimal=minorToDecimal(Math.abs(value),currencyExponent(currency));return value<0?`-${decimal}`:decimal}export function moneyInputStep(currency){const exponent=currencyExponent(currency);return exponent===0?"1":`0.${"1".padStart(exponent,"0")}`}export function moneyInputError(input,currency,options={}){const raw=input===null||input===void 0?"":String(input).trim(),exponent=currencyExponent(currency),code=(currency||"USD").toUpperCase();if(raw==="")return options.required===!1?"":"Enter an amount.";const match=/^(\d*)(?:\.(\d*))?$/.exec(raw);if(!match||!/\d/.test(raw))return exponent===0?"Enter a whole number, for example 1999.":`Enter a plain amount, for example ${minorToDecimal(1999,exponent)}.`;if((match[2]??"").length>exponent)return exponent===0?`${code} amounts have no decimal places.`:`${code} amounts have at most ${exponent} decimal ${exponent===1?"place":"places"}.`;let minor;try{minor=decimalToMinor(raw,exponent)}catch{return"That amount is too large."}if(minor===null)return"Enter an amount.";if(options.min!==void 0&&minor<options.min)return`Enter at least ${formatMinor(options.min,code)}.`;if(options.max!==void 0&&minor>options.max)return`Enter at most ${formatMinor(options.max,code)}.`;return""}export function parseMoneyInput(input,currency,options={}){const error=moneyInputError(input,currency,options);if(error)throw new PriceFormatError(input,error);return decimalToMinor(input,currencyExponent(currency))}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,matchedRows}from"@stacksjs/database/runtime";import{mutationCount}from"../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("orders").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete order: ${error.message}`);throw error}}export async function softDelete(id){try{return(await matchedRows(db.updateTable("orders").set({status:"CANCELED"}).where("id","=",id))).length>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to soft delete order: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("orders").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete orders: ${error.message}`);throw error}}export async function bulkSoftDelete(ids){if(!ids.length)return 0;try{return(await matchedRows(db.updateTable("orders").set({status:"CANCELED"}).where("id","in",ids))).length}catch(error){if(error instanceof Error)throw TypeError(`Failed to soft delete orders: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const TRANSITIONS={PENDING:["PROCESSING","CANCELLED"],PROCESSING:["SHIPPED","OUT_FOR_DELIVERY","CANCELLED","REFUNDED"],SHIPPED:["OUT_FOR_DELIVERY","DELIVERED","REFUNDED"],OUT_FOR_DELIVERY:["DELIVERED","SHIPPED","CANCELLED","REFUNDED"],DELIVERED:["REFUNDED"],CANCELLED:["PROCESSING"],REFUNDED:[]};export function canTransition(from,to){if(from===to)return!0;return TRANSITIONS[from]?.includes(to)??!1}async function emitOrderEvent(eventName,payload){try{const mod=await import("@stacksjs/events").catch(()=>null);if(!mod)return;const dispatch=mod.dispatch;if(typeof dispatch!=="function")return;dispatch(eventName,payload)}catch{}}export async function emitOrderCreated(order){await emitOrderEvent("order:created",{order})}export async function emitOrderPaid(order,payment){await emitOrderEvent("order:paid",{order,payment})}export async function emitOrderShipped(order){await emitOrderEvent("order:shipped",{order})}export async function emitOrderOutForDelivery(order){await emitOrderEvent("order:out_for_delivery",{order})}export async function emitOrderDelivered(order){await emitOrderEvent("order:delivered",{order})}export async function emitOrderCancelled(order,reason){await emitOrderEvent("order:cancelled",{order,reason})}export async function emitOrderRefunded(order,refundAmount){await emitOrderEvent("order:refunded",{order,refundAmount})}export async function emitForStatus(status,order){switch(status){case"PROCESSING":return emitOrderPaid(order);case"SHIPPED":return emitOrderShipped(order);case"OUT_FOR_DELIVERY":return emitOrderOutForDelivery(order);case"DELIVERED":return emitOrderDelivered(order);case"CANCELLED":return emitOrderCancelled(order);case"REFUNDED":return emitOrderRefunded(order);case"PENDING":default:}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{createSpreadsheet}from"ts-spreadsheets";import{formatMinor}from"../money";export async function exportOrders(format="csv"){const orders=await fetchOrdersForExport(),spreadsheetData=prepareOrdersForExport(orders);return createSpreadsheet(spreadsheetData,{type:format})}const ID_CHUNK=500;function chunks(values,size=ID_CHUNK){const result=[];for(let index=0;index<values.length;index+=size)result.push(values.slice(index,index+size));return result}function uniqueIds(values){const ids=new Set;for(const value of values){const id=Number(value);if(Number.isSafeInteger(id)&&id>0)ids.add(id)}return[...ids]}export async function fetchOrdersForExport(){const orders=await db.selectFrom("orders").selectAll().orderBy("id","asc").execute();if(orders.length===0)return[];const orderIds=uniqueIds(orders.map((order)=>order.id)),customerIds=uniqueIds(orders.map((order)=>order.customer_id)),customers=new Map;for(const ids of chunks(customerIds)){const rows=await db.selectFrom("customers").where("id","in",ids).select(["id","name","email"]).execute();for(const row of rows)customers.set(Number(row.id),row)}const items=[];for(const ids of chunks(orderIds)){const rows=await db.selectFrom("order_items").where("order_id","in",ids).select(["id","order_id","product_id","quantity","price"]).orderBy("id","asc").execute();items.push(...rows)}const products=new Map;for(const ids of chunks(uniqueIds(items.map((item)=>item.product_id)))){const rows=await db.selectFrom("products").where("id","in",ids).select(["id","name"]).execute();for(const row of rows)products.set(Number(row.id),row)}const itemsByOrder=new Map;for(const item of items){const orderId=Number(item.order_id),line={...item,product:products.get(Number(item.product_id))},lines=itemsByOrder.get(orderId);if(lines)lines.push(line);else itemsByOrder.set(orderId,[line])}return orders.map((order)=>{const lines=itemsByOrder.get(Number(order.id))??[],customer=customers.get(Number(order.customer_id));return{...order,customer,customer_name:customer?.name??null,customer_email:customer?.email??null,order_items:lines,totalItems:lines.reduce((sum,line)=>sum+Number(line.quantity??0),0),totalPrice:lines.reduce((sum,line)=>sum+Number(line.price??0)*Number(line.quantity??0),0)}})}export function prepareOrdersForExport(orders){const headings=["Order ID","Customer","Date","Total","Status","Items"],data=orders.map((order)=>{const currency=typeof order.currency==="string"&&order.currency?order.currency:null,itemsString=order.order_items?.map((item)=>`${item.product?.name||`Product #${item.product_id??"?"}`} (Qty: ${item.quantity}, Price: ${formatMinor(Number(item.price??0),currency)})`).join(" | ")||"No Items";return[order.id,order.customer?.name||order.customer_name||"N/A",order.created_at,formatMinor(Number(order.total_amount??0),currency),order.status,itemsString]});return{headings,data}}export async function downloadOrders(format="csv",filename){const spreadsheet=await exportOrders(format),defaultFilename=`orders_export_${new Date().toISOString().split("T")[0]}.${format}`;return spreadsheet.download(filename||defaultFilename)}export async function storeOrdersExport(format="csv",path){const spreadsheet=await exportOrders(format),defaultPath=`orders_export_${new Date().toISOString().split("T")[0]}.${format}`;spreadsheet.store(path||defaultPath)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../utils/model-row";export async function fetchAll(limit){let query=db.selectFrom("orders").selectAll().orderBy("created_at","desc");if(limit)query=query.limit(limit);const orders=asModelRows(await query.execute());return await Promise.all(orders.map(async(order)=>{const items=await db.selectFrom("order_items").where("order_id","=",order.id).selectAll().execute();return{...order,items}}))}export async function fetchRecent(limit=10){return fetchAll(limit)}export async function fetchById(id){const order=await db.selectFrom("orders").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(order,!0)}export async function fetchStats(){const totalOrders=await db.selectFrom("orders").select(db.fn.count("id").as("count")).executeTakeFirst(),ordersByStatus=await db.selectFrom("orders").select(["status",db.fn.count("id").as("count")]).groupBy("status").execute(),ordersByType=await db.selectFrom("orders").select(["order_type",db.fn.count("id").as("count")]).groupBy("order_type").execute(),recentOrdersRaw=asModelRows(await db.selectFrom("orders").selectAll().orderBy("created_at","desc").limit(5).execute()),recentOrders=await Promise.all(recentOrdersRaw.map(async(order)=>{const items=await db.selectFrom("order_items").where("order_id","=",order.id).selectAll().execute();return{...order,items}})),revenue=await db.selectFrom("orders").select(db.fn.sum("total_amount").as("total")).executeTakeFirst();return{total:Number(totalOrders?.count||0),by_status:ordersByStatus,by_type:ordersByType,recent:recentOrders,revenue:Number(revenue?.total||0)}}export async function compareOrdersByPeriod(_daysRange=30){const today=new Date,currentPeriodStart=new Date(today);currentPeriodStart.setDate(today.getDate()-_daysRange);const previousPeriodEnd=new Date(currentPeriodStart);previousPeriodEnd.setDate(previousPeriodEnd.getDate()-1);const previousPeriodStart=new Date(previousPeriodEnd);previousPeriodStart.setDate(previousPeriodEnd.getDate()-_daysRange);const currentPeriodOrders=await db.selectFrom("orders").select(db.fn.count("id").as("count")).where("created_at",">=",currentPeriodStart.toISOString()).where("created_at","<=",today.toISOString()).executeTakeFirst(),previousPeriodOrders=await db.selectFrom("orders").select(db.fn.count("id").as("count")).where("created_at",">=",previousPeriodStart.toISOString()).where("created_at","<=",previousPeriodEnd.toISOString()).executeTakeFirst(),currentCount=Number(currentPeriodOrders?.count||0),previousCount=Number(previousPeriodOrders?.count||0),difference=currentCount-previousCount,percentageChange=previousCount!==0?difference/previousCount*100:currentCount>0?100:0;return{current_period:currentCount,previous_period:previousCount,difference,percentage_change:percentageChange,days_range:_daysRange}}export async function calculateOrderMetrics(_daysRange=30){const today=new Date,currentPeriodStart=new Date(today);currentPeriodStart.setDate(today.getDate()-_daysRange);const previousPeriodEnd=new Date(currentPeriodStart);previousPeriodEnd.setDate(previousPeriodEnd.getDate()-1);const previousPeriodStart=new Date(previousPeriodEnd);previousPeriodStart.setDate(previousPeriodEnd.getDate()-_daysRange);const currentPeriodValues=await db.selectFrom("orders").select([db.fn.count("id").as("total_orders"),db.fn.sum("total_amount").as("total_revenue")]).where("created_at",">=",currentPeriodStart.toISOString()).where("created_at","<=",today.toISOString()).executeTakeFirst(),previousPeriodValues=await db.selectFrom("orders").select([db.fn.count("id").as("total_orders"),db.fn.sum("total_amount").as("total_revenue")]).where("created_at",">=",previousPeriodStart.toISOString()).where("created_at","<=",previousPeriodEnd.toISOString()).executeTakeFirst(),ordersByStatus=await db.selectFrom("orders").select(["status",db.fn.count("id").as("count")]).where("created_at",">=",currentPeriodStart.toISOString()).where("created_at","<=",today.toISOString()).groupBy("status").execute(),ordersByType=await db.selectFrom("orders").select(["order_type",db.fn.count("id").as("count")]).where("created_at",">=",currentPeriodStart.toISOString()).where("created_at","<=",today.toISOString()).groupBy("order_type").execute(),currentTotalOrders=Number(currentPeriodValues?.total_orders||0),currentTotalRevenue=Number(currentPeriodValues?.total_revenue||0),currentAverageOrderValue=currentTotalOrders>0?currentTotalRevenue/currentTotalOrders:0,previousTotalOrders=Number(previousPeriodValues?.total_orders||0),previousTotalRevenue=Number(previousPeriodValues?.total_revenue||0),previousAverageOrderValue=previousTotalOrders>0?previousTotalRevenue/previousTotalOrders:0,ordersDifference=currentTotalOrders-previousTotalOrders,revenueDifference=currentTotalRevenue-previousTotalRevenue,aovDifference=currentAverageOrderValue-previousAverageOrderValue,ordersPercentageChange=previousTotalOrders!==0?ordersDifference/previousTotalOrders*100:currentTotalOrders>0?100:0,revenuePercentageChange=previousTotalRevenue!==0?revenueDifference/previousTotalRevenue*100:currentTotalRevenue>0?100:0,aovPercentageChange=previousAverageOrderValue!==0?aovDifference/previousAverageOrderValue*100:currentAverageOrderValue>0?100:0;return{current_period:{total_orders:currentTotalOrders,total_revenue:currentTotalRevenue,average_order_value:currentAverageOrderValue,orders_by_status:ordersByStatus.map((item)=>({status:item.status,count:Number(item.count)})),orders_by_type:ordersByType.map((item)=>({order_type:item.order_type,count:Number(item.count)}))},previous_period:{total_orders:previousTotalOrders,total_revenue:previousTotalRevenue,average_order_value:previousAverageOrderValue},comparison:{orders:{difference:ordersDifference,percentage:Math.abs(ordersPercentageChange),is_increase:ordersDifference>=0},revenue:{difference:revenueDifference,percentage:Math.abs(revenuePercentageChange),is_increase:revenueDifference>=0},average_order_value:{difference:aovDifference,percentage:Math.abs(aovPercentageChange),is_increase:aovDifference>=0}},days_range:_daysRange}}export async function fetchDailyOrderTrends(_daysRange=30){const today=new Date,startDate=new Date(today);startDate.setDate(today.getDate()-_daysRange);const orders=await db.selectFrom("orders").select(["created_at","total_amount"]).where("created_at",">=",startDate.toISOString()).where("created_at","<=",today.toISOString()).orderBy("created_at","asc").execute(),dailyMap=new Map;for(const order of orders){const date=(order.created_at||"").split("T")[0]||"unknown",entry=dailyMap.get(date)||{order_count:0,revenue:0};entry.order_count++;entry.revenue+=Number(order.total_amount||0);dailyMap.set(date,entry)}return Array.from(dailyMap.entries()).map(([date,stats])=>({date,order_count:stats.order_count,revenue:stats.revenue}))}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,sqlHelpers}from"@stacksjs/database/runtime";import{env}from"@stacksjs/env";import{mutationCount}from"../utils/mutation-count";export function validateCouponStacking(couponIds,policy={}){const count=couponIds.length;if(count<=1)return{ok:!0,count};if(!policy.allowMultiple)return{ok:!1,count,reason:"multiple-not-allowed"};const cap=policy.maxStacked??Number.POSITIVE_INFINITY;if(count>cap)return{ok:!1,count,reason:"exceeds-max-stacked"};return{ok:!0,count}}export async function validateQuantityBounds(productId,quantity){if(!Number.isFinite(quantity)||quantity<=0)return{ok:!1,productId,quantity,reason:"non-positive"};let product;try{product=await db.selectFrom("products").where("id","=",productId).select(["min_order_qty","max_order_qty"]).executeTakeFirst()}catch{return{ok:!0,productId,quantity}}if(!product)return{ok:!1,productId,quantity,reason:"product-missing"};const min=product.min_order_qty??null,max=product.max_order_qty??null;if(min!=null&&quantity<min)return{ok:!1,productId,quantity,reason:"below-min",bound:min};if(max!=null&&quantity>max)return{ok:!1,productId,quantity,reason:"above-max",bound:max};return{ok:!0,productId,quantity}}export async function validateCartQuantities(items){for(const item of items){const r=await validateQuantityBounds(item.productId,item.quantity);if(!r.ok)return r}const last=items[items.length-1]??{productId:0,quantity:0};return{ok:!0,productId:last.productId,quantity:last.quantity}}export async function cleanupAbandonedCarts(options={}){const olderThanDays=options.olderThanDays??30,limit=options.limit??1000,cutoffAt=new Date(Date.now()-olderThanDays*24*60*60*1000).toISOString().slice(0,19).replace("T"," "),p=sqlHelpers(env.DB_CONNECTION||"sqlite").param;try{const statement=await db.unsafe(`DELETE FROM carts WHERE updated_at < ${p(1)} AND id IN (SELECT id FROM carts WHERE updated_at < ${p(2)} LIMIT ${p(3)})`,[cutoffAt,cutoffAt,limit]);return{deleted:mutationCount(statement),cutoffAt}}catch{try{const rows=await db.selectFrom("carts").where("updated_at","<",cutoffAt).select(["id"]).limit(limit).execute();if(rows.length===0)return{deleted:0,cutoffAt};const ids=rows.map((r)=>r.id),result=await db.deleteFrom("carts").where("id","in",ids).execute();return{deleted:mutationCount(result),cutoffAt}}catch{return{deleted:0,cutoffAt}}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,bulkSoftDelete,destroy,softDelete}from"./destroy";export{downloadOrders,exportOrders,storeOrdersExport}from"./export";export{calculateOrderMetrics,compareOrdersByPeriod,fetchAll,fetchById,fetchDailyOrderTrends,fetchRecent,fetchStats}from"./fetch";export{store}from"./store";export{findOrderByIdempotencyKey,placeOrder}from"./place-order";export{transitionStatus}from"./transition";export{canTransition,emitForStatus,emitOrderCancelled,emitOrderCreated,emitOrderDelivered,emitOrderPaid,emitOrderRefunded,emitOrderShipped}from"./events";export{update,updateDeliveryInfo,updateStatus}from"./update";export{handleCommercePaymentEvent,handlePaymentFailed,handlePaymentSucceeded,handleRefundSucceeded,receivePaymentWebhook}from"./webhook";export{recomputeOrderTotals}from"./totals";export{cleanupAbandonedCarts,validateCartQuantities,validateCouponStacking,validateQuantityBounds}from"./guards";
|
|
@@ -0,0 +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}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../utils/model-row";import{formatDate}from"@stacksjs/orm";import{insertedId}from"../utils/inserted-id";export async function store(data){const orderData={...data,status:data.status||"PENDING",uuid:randomUUIDv7(),created_at:formatDate(new Date),updated_at:formatDate(new Date)};try{const createdOrder=await db.insertInto("orders").values(orderData).executeTakeFirst(),id=insertedId(createdOrder);if(id!==void 0){const order=await db.selectFrom("orders").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(order,!0)}const order=await db.selectFrom("orders").where("uuid","=",orderData.uuid).selectAll().executeTakeFirst();return asModelRow(order,!0)}catch(error){if(error instanceof Error)throw Error(`Failed to create order: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";async function fetchProductPrices(ids){if(ids.length===0)return new Map;const rows=await db.selectFrom("products").where("id","in",ids).select(["id","price"]).execute(),out=new Map;for(const r of rows){const cents=Number(r.price);if(Number.isFinite(cents))out.set(r.id,Math.round(cents))}return out}async function fetchTaxRate(id){try{const row=await db.selectFrom("tax_rates").where("id","=",id).select(["rate"]).executeTakeFirst(),rate=Number(row?.rate??0);return Number.isFinite(rate)?rate:0}catch{return 0}}export async function recomputeOrderTotals(input){const productIds=input.items.map((i)=>i.productId),priceMap=await fetchProductPrices(productIds),taxRate=input.taxRateId!=null?await fetchTaxRate(input.taxRateId):0;let subtotalCents=0;const resolvedItems=[];for(const item of input.items){const unitCents=item.unitPriceCents??priceMap.get(item.productId)??0,lineCents=Math.round(unitCents*item.quantity);subtotalCents+=lineCents;resolvedItems.push({productId:item.productId,quantity:item.quantity,unitPriceCents:unitCents,lineCents})}const discountCents=Math.max(0,input.discountCents??0),taxableCents=Math.max(0,subtotalCents-discountCents),taxCents=Math.round(taxableCents*taxRate),shippingCents=Math.max(0,input.shippingCents??0),totalCents=subtotalCents-discountCents+taxCents+shippingCents,maxDrift=input.maxDriftCents??5;let priceChanged=!1,diffCents=0;if(input.clientTotalCents!=null){diffCents=Math.abs(totalCents-input.clientTotalCents);priceChanged=diffCents>maxDrift}return{subtotalCents,discountCents,taxCents,shippingCents,totalCents,resolvedItems,priceChanged,diffCents}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,enqueueAfterCommit}from"@stacksjs/database/runtime";import{canTransition,emitForStatus}from"./events";export async function transitionStatus(id,from,to,options={}){if(!canTransition(from,to))throw Error(`Illegal order transition ${from} -> ${to}`);if(from===to)return!1;return db.transaction(async(trx)=>{if(!(await db.updateTable("orders").set({...options.values,status:to,updated_at:new Date().toISOString()}).where("id","=",id).where("status","=",from).returning("id").execute()).length)return!1;const order=await db.selectFrom("orders").selectAll().where("id","=",id).executeTakeFirst();if(!order)throw Error(`Order ${id} disappeared during transition`);await options.apply?.(trx,order);const emit=()=>emitForStatus(to,order);if(!enqueueAfterCommit(emit))await emit();return!0})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";import{fetchById}from"./fetch";import{canTransition}from"./events";export{transitionStatus}from"./transition";import{transitionStatus}from"./transition";export async function update(id,data){if(!await fetchById(id))return;try{await db.updateTable("orders").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update order: ${error.message}`);throw error}}export async function updateStatus(id,status){const order=await fetchById(id);if(!order)throw Error(`Order with ID ${id} not found`);const currentStatus=order.status??"PENDING";if(!canTransition(currentStatus,status))throw Error(`[commerce/orders] illegal status transition ${currentStatus} \u2192 ${status}`);try{await transitionStatus(id,currentStatus,status);return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update order status: ${error.message}`);throw error}}export async function updateDeliveryInfo(id,delivery_address,estimated_delivery_time){const order=await fetchById(id);if(!order)throw Error(`Order with ID ${id} not found`);const updateData={updated_at:formatDate(new Date)};if(delivery_address!==void 0)updateData.delivery_address=delivery_address;if(estimated_delivery_time!==void 0)updateData.estimated_delivery_time=estimated_delivery_time;if(Object.keys(updateData).length===1)return order;try{await db.updateTable("orders").set(updateData).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update delivery information: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";import{canTransition,emitOrderCancelled,emitOrderPaid,emitOrderRefunded}from"./events";async function findOrderByPaymentIntent(paymentIntentId){const payment=await db.selectFrom("payments").where("transaction_id","=",paymentIntentId).selectAll().executeTakeFirst();if(!payment)return null;return await db.selectFrom("orders").where("id","=",payment.order_id).selectAll().executeTakeFirst()??null}let warnedAboutMissingDedupTable=!1;async function recordEventOrSkip(event,trx){try{const now=formatDate(new Date);await trx.insertInto("payment_webhook_events").values({provider:event.provider,event_id:event.id,processed_at:now,created_at:now}).execute();return!0}catch(err){if(isUniqueViolation(err))return!1;if(isMissingTableError(err)){if(!warnedAboutMissingDedupTable){warnedAboutMissingDedupTable=!0;console.warn("[commerce/webhook] payment_webhook_events table missing - provider retries will be applied again. Run `buddy migrate` to enable dedup.")}return!0}throw err}}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)}function isUniqueViolation(err){const e=err,msg=e?.message??"";return e?.code==="23505"||msg.includes("UNIQUE constraint")||msg.includes("Duplicate entry")||/duplicate key value/i.test(msg)}async function handlePaymentSucceeded(event){const paymentIntentId=event.reference;if(!paymentIntentId)return;await db.transaction(async(trx)=>{if(!await recordEventOrSkip(event,trx))return;const payment=await trx.selectFrom("payments").where("transaction_id","=",paymentIntentId).selectAll().executeTakeFirst();if(!payment)return;const now=formatDate(new Date);await trx.updateTable("payments").set({status:"succeeded",updated_at:now}).where("id","=",payment.id).execute();const order=await trx.selectFrom("orders").where("id","=",payment.order_id).selectAll().executeTakeFirst();if(order&&order.status==="PENDING")await trx.updateTable("orders").set({status:"PROCESSING",updated_at:now}).where("id","=",order.id).execute()});const order=await findOrderByPaymentIntent(paymentIntentId);if(order)await emitOrderPaid(order)}async function handlePaymentFailed(event){const paymentIntentId=event.reference;if(!paymentIntentId)return;const reason=event.reason??"payment failed";await db.transaction(async(trx)=>{if(!await recordEventOrSkip(event,trx))return;const payment=await trx.selectFrom("payments").where("transaction_id","=",paymentIntentId).selectAll().executeTakeFirst();if(!payment)return;const now=formatDate(new Date);await trx.updateTable("payments").set({status:"failed",failure_reason:reason.slice(0,1000),updated_at:now}).where("id","=",payment.id).execute();const order=await trx.selectFrom("orders").where("id","=",payment.order_id).selectAll().executeTakeFirst();if(order&&order.status==="PENDING")await trx.updateTable("orders").set({status:"CANCELLED",updated_at:now}).where("id","=",order.id).execute()});const order=await findOrderByPaymentIntent(paymentIntentId);if(order)await emitOrderCancelled(order,reason)}async function handleRefundSucceeded(event){const paymentIntentId=event.reference;if(!paymentIntentId)return;const statedTotal=event.refundedTotal?.amount,given=statedTotal??event.amount?.amount??0;if(!Number.isSafeInteger(given)||given<=0)return;let fullyRefunded=!1,applied=!1,refundAmount=0;await db.transaction(async(trx)=>{if(!await recordEventOrSkip(event,trx))return;const payment=await trx.selectFrom("payments").where("transaction_id","=",paymentIntentId).selectAll().executeTakeFirst();if(!payment)return;const already=Number(payment.refund_amount??0);refundAmount=statedTotal===void 0?given:statedTotal-already;if(refundAmount<=0)return;const total=already+refundAmount;if(total>Number(payment.amount)){console.warn(`[commerce/webhook] ${event.provider} refunded ${refundAmount} of payment ${payment.id}, which has ${Number(payment.amount)-already} left to refund; not recorded.`);return}fullyRefunded=total===Number(payment.amount);const now=formatDate(new Date);await trx.updateTable("payments").set({status:fullyRefunded?"refunded":"partiallyRefunded",refund_amount:total,updated_at:now}).where("id","=",payment.id).execute();applied=!0;if(!fullyRefunded)return;const order=await trx.selectFrom("orders").where("id","=",payment.order_id).selectAll().executeTakeFirst();if(order&&canTransition(order.status,"REFUNDED"))await trx.updateTable("orders").set({status:"REFUNDED",updated_at:now}).where("id","=",order.id).execute()});if(!applied)return;const order=await findOrderByPaymentIntent(paymentIntentId);if(order&&fullyRefunded)await emitOrderRefunded(order,refundAmount)}export async function handleCommercePaymentEvent(event){switch(event.type){case"payment.succeeded":await handlePaymentSucceeded(event);return!0;case"payment.failed":await handlePaymentFailed(event);return!0;case"refund.succeeded":await handleRefundSucceeded(event);return!0;default:return!1}}export async function receivePaymentWebhook(request,driver){const{paymentDriver,WebhookNotConfiguredError,WebhookSignatureError}=await import("@stacksjs/payments"),payments=driver??paymentDriver();let events;try{events=await payments.verifyWebhook({payload:await request.text(),headers:request.headers})}catch(error){if(error instanceof WebhookNotConfiguredError)return Response.json({ok:!1,reason:"missing-config"},{status:401});if(error instanceof WebhookSignatureError)return Response.json({ok:!1,reason:"invalid-signature"},{status:400});throw error}for(const event of events)await handleCommercePaymentEvent(event);return payments.acknowledgeWebhook()}export{handlePaymentFailed,handlePaymentSucceeded,handleRefundSucceeded};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{mutationCount}from"../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("payments").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete payment: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("payments").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete payments: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../utils/model-row";import{formatDate}from"@stacksjs/orm";export async function fetchAll(){const rows=await db.selectFrom("payments").selectAll().execute();return asModelRows(rows)}export async function fetchById(id){const row=await db.selectFrom("payments").selectAll().where("id","=",id).executeTakeFirst();return asModelRow(row,!0)}export async function fetchPaymentStats(daysRange=30){const today=new Date,currentPeriodStart=new Date(today);currentPeriodStart.setDate(today.getDate()-daysRange);const previousPeriodEnd=new Date(currentPeriodStart);previousPeriodEnd.setDate(previousPeriodEnd.getDate()-1);const previousPeriodStart=new Date(previousPeriodEnd);previousPeriodStart.setDate(previousPeriodEnd.getDate()-daysRange);const currentStats=await db.selectFrom("payments").select([db.fn.count("id").as("transaction_count"),db.fn.sum("amount").as("total_revenue")]).where("created_at",">=",formatDate(currentPeriodStart)).where("created_at","<=",formatDate(today)).where("status","=","completed").executeTakeFirst(),previousStats=await db.selectFrom("payments").select([db.fn.count("id").as("transaction_count"),db.fn.sum("amount").as("total_revenue")]).where("created_at",">=",formatDate(previousPeriodStart)).where("created_at","<=",formatDate(previousPeriodEnd)).where("status","=","completed").executeTakeFirst(),totalTransactions=await db.selectFrom("payments").select(db.fn.count("id").as("count")).where("created_at",">=",formatDate(currentPeriodStart)).where("created_at","<=",formatDate(today)).executeTakeFirst(),currentTransactions=Number(currentStats?.transaction_count||0),currentRevenue=Number(currentStats?.total_revenue||0),currentAverage=currentTransactions>0?currentRevenue/currentTransactions:0,previousTransactions=Number(previousStats?.transaction_count||0),previousRevenue=Number(previousStats?.total_revenue||0),previousAverage=previousTransactions>0?previousRevenue/previousTransactions:0,transactionDifference=currentTransactions-previousTransactions,revenueDifference=currentRevenue-previousRevenue,averageDifference=currentAverage-previousAverage,transactionPercentage=previousTransactions>0?transactionDifference/previousTransactions*100:currentTransactions>0?100:0,revenuePercentage=previousRevenue>0?revenueDifference/previousRevenue*100:currentRevenue>0?100:0,averagePercentage=previousAverage>0?averageDifference/previousAverage*100:currentAverage>0?100:0,allTransactions=Number(totalTransactions?.count||0),successRate=allTransactions>0?currentTransactions/allTransactions*100:0;return{total_transactions:currentTransactions,total_revenue:currentRevenue,average_transaction:currentAverage,successful_rate:successRate,comparison:{transactions:{difference:transactionDifference,percentage:Math.abs(transactionPercentage),is_increase:transactionDifference>=0},revenue:{difference:revenueDifference,percentage:Math.abs(revenuePercentage),is_increase:revenueDifference>=0},average:{difference:averageDifference,percentage:Math.abs(averagePercentage),is_increase:averageDifference>=0}}}}export async function fetchPaymentStatsByMethod(_daysRange=30){const today=new Date,startDate=new Date(today);startDate.setDate(today.getDate()-_daysRange);const totalStats=await db.selectFrom("payments").select([db.fn.count("id").as("total_count"),db.fn.sum("amount").as("total_revenue")]).where("created_at",">=",formatDate(startDate)).where("created_at","<=",formatDate(today)).where("status","=","completed").executeTakeFirst(),totalCount=Number(totalStats?.total_count||0),methodStats=await db.selectFrom("payments").select(["method",db.fn.count("id").as("count"),db.fn.sum("amount").as("revenue")]).where("created_at",">=",formatDate(startDate)).where("created_at","<=",formatDate(today)).where("status","=","completed").groupBy("method").execute(),result={};methodStats.forEach((item)=>{const count=Number(item.count||0),revenue=Number(item.revenue||0),percentageOfTotal=totalCount>0?count/totalCount*100:0;result[item.method]={count,revenue,percentage_of_total:percentageOfTotal}});return result}export async function fetchMonthlyPaymentTrends(){const today=new Date,twelveMonthsAgo=new Date(today);twelveMonthsAgo.setMonth(today.getMonth()-11);twelveMonthsAgo.setDate(1);const payments=await db.selectFrom("payments").selectAll().where("created_at",">=",formatDate(twelveMonthsAgo)).where("status","=","completed").execute(),monthlyMap={};for(const payment of payments){const createdAt=payment.created_at?new Date(String(payment.created_at)):new Date,year=createdAt.getFullYear(),month=createdAt.getMonth()+1,key=`${year}-${String(month).padStart(2,"0")}`;if(!monthlyMap[key])monthlyMap[key]={year,month,transactions:0,revenue:0};monthlyMap[key].transactions++;monthlyMap[key].revenue+=Number(payment.amount||0)}return Object.keys(monthlyMap).sort().map((key)=>{const item=monthlyMap[key],{transactions,revenue}=item,average=transactions>0?revenue/transactions:0;return{month:new Date(item.year,item.month-1,1).toLocaleString("default",{month:"short"}),year:item.year,transactions,revenue,average}})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,destroy}from"./destroy";export{fetchAll,fetchById,fetchMonthlyPaymentTrends,fetchPaymentStats,fetchPaymentStatsByMethod}from"./fetch";export{store}from"./store";export{recordRefund,update}from"./update";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../utils/model-row";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";export async function store(data){const uuid=randomUUIDv7(),paymentData={...data,status:data.status||"pending",currency:data.currency||"USD",uuid};try{await db.insertInto("payments").values(paymentData).executeTakeFirst();const row=await db.selectFrom("payments").where("uuid","=",uuid).selectAll().executeTakeFirst();return asModelRow(row,!0)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A payment with this transaction ID already exists");if(error instanceof Error){if(error.message.includes("Insufficient funds"))throw Error("Insufficient funds for this payment");throw Error(`Failed to create payment: ${error.message}`)}throw error}}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{db,mutationCount,sqlHelpers}from"@stacksjs/database/runtime";import{env}from"@stacksjs/env";import{formatDate}from"@stacksjs/orm";import{fetchById}from"./fetch";export async function update(id,data){if(!await fetchById(id))return;try{await db.updateTable("payments").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error){if(error.message.includes("Duplicate entry")&&error.message.includes("transaction_id"))throw Error("A payment with this transaction ID already exists");throw Error(`Failed to update payment: ${error.message}`)}throw error}}const REFUNDABLE_STATUSES=new Set(["completed","succeeded","partiallyRefunded"]);export async function recordRefund(id,amount){if(!Number.isSafeInteger(id)||id<=0)throw TypeError("Payment ID must be a positive integer");if(!Number.isSafeInteger(amount)||amount<=0)throw TypeError("Refund amount must be a positive integer in minor units");const p=sqlHelpers(env.DB_CONNECTION||"sqlite").param,now=formatDate(new Date),statement=await db.unsafe(`UPDATE payments
|
|
2
|
+
SET status = CASE WHEN COALESCE(refund_amount, 0) + ${p(1)} = amount THEN 'refunded'
|
|
3
|
+
WHEN 1 = 0 THEN status ELSE 'partiallyRefunded' END,
|
|
4
|
+
refund_amount = COALESCE(refund_amount, 0) + ${p(2)},
|
|
5
|
+
updated_at = ${p(3)}
|
|
6
|
+
WHERE id = ${p(4)}
|
|
7
|
+
AND status IN ('completed', 'succeeded', 'partiallyRefunded')
|
|
8
|
+
AND COALESCE(refund_amount, 0) + ${p(5)} <= amount`,[amount,amount,now,id,amount]);if(mutationCount(statement)>0){const updated=await fetchById(id);if(updated)return updated;throw Error(`Payment with ID ${id} disappeared during refund`)}const payment=await fetchById(id);if(!payment)throw Error(`Payment with ID ${id} not found`);const status=String(payment.status||"");if(!REFUNDABLE_STATUSES.has(status))throw Error(`Payment status ${status||"unknown"} cannot be refunded`);const persisted=payment,remainingAmount=Number(payment.amount||0)-Number(persisted.refundAmount??persisted.refund_amount??0);throw Error(`Refund amount exceeds the remaining ${remainingAmount} minor units`)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/product.js
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,matchedRows}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";import{fetchById}from"./fetch";import{mutationCount}from"../../utils/mutation-count";export async function remove(id){const result=await db.deleteFrom("categories").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}export async function bulkRemove(ids){if(!ids.length)return 0;const result=await db.deleteFrom("categories").where("id","in",ids).executeTakeFirst();return mutationCount(result)}export async function removeChildCategories(parentId){const result=await db.deleteFrom("categories").where("parent_category_id","=",parentId).executeTakeFirst();return mutationCount(result)}export async function deactivate(id){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);const updated=await db.updateTable("categories").set({is_active:!1,updated_at:formatDate(new Date)}).where("id","=",id).returning("id").executeTakeFirst();return Boolean(updated)}export async function deactivateChildCategories(parentId){return(await matchedRows(db.updateTable("categories").set({is_active:!1,updated_at:formatDate(new Date)}).where("parent_category_id","=",parentId))).length}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";export async function fetchAll(){return await db.selectFrom("categories").selectAll().execute()}export async function fetchById(id){return await db.selectFrom("categories").where("id","=",id).selectAll().executeTakeFirst()}export async function fetchByName(name){return await db.selectFrom("categories").where("name","=",name).selectAll().executeTakeFirst()}export async function fetchActive(){return await db.selectFrom("categories").where("is_active","=",!0).selectAll().execute()}export async function fetchRootCategories(){return await db.selectFrom("categories").whereNull("parent_category_id").where("is_active","=",!0).selectAll().execute()}export async function fetchChildCategories(parentId){return await db.selectFrom("categories").where("parent_category_id","=",parentId).where("is_active","=",!0).selectAll().execute()}export async function fetchByDisplayOrder(ascending=!0){const query=db.selectFrom("categories").where("is_active","=",!0).selectAll();if(ascending)return await query.orderBy("display_order","asc").execute();else return await query.orderBy("display_order","desc").execute()}export async function fetchStats(){const totalCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).executeTakeFirst(),activeCategories=await db.selectFrom("categories").where("is_active","=",!0).select(db.fn.count("id").as("count")).executeTakeFirst(),rootCategories=await db.selectFrom("categories").whereNull("parent_category_id").select(db.fn.count("id").as("count")).executeTakeFirst(),categoriesWithImages=await db.selectFrom("categories").whereNotNull("image_url").select(db.fn.count("id").as("count")).executeTakeFirst(),thirtyDaysAgo=new Date;thirtyDaysAgo.setDate(thirtyDaysAgo.getDate()-30);const thirtyDaysAgoStr=formatDate(thirtyDaysAgo),recentlyAddedCategories=await db.selectFrom("categories").where("created_at",">=",thirtyDaysAgoStr).selectAll().limit(5).execute(),categoriesByParent=await db.selectFrom("categories as c").leftJoin("categories as parent","c.parent_category_id","=","parent.id").whereNotNull("c.parent_category_id").select(["c.parent_category_id","parent.name as parent_name",db.fn.count("c.id").as("child_count")]).groupBy(["c.parent_category_id","parent.name"]).orderBy("child_count","desc").limit(5).execute();return{total:Number(totalCategories?.count||0),active:Number(activeCategories?.count||0),root_categories:Number(rootCategories?.count||0),child_categories:Number(totalCategories?.count||0)-Number(rootCategories?.count||0),with_images:Number(categoriesWithImages?.count||0),recently_added:recentlyAddedCategories,top_parent_categories:categoriesByParent.map((item)=>({id:String(item.parent_category_id||""),name:String(item.parent_name||""),child_count:Number(item.child_count)}))}}export async function compareCategoryGrowth(_daysRange=30){const today=new Date,todayStr=formatDate(today),currentPeriodStart=new Date;currentPeriodStart.setDate(today.getDate()-_daysRange);const currentPeriodStartStr=formatDate(currentPeriodStart),previousPeriodEnd=new Date(currentPeriodStart);previousPeriodEnd.setDate(previousPeriodEnd.getDate()-1);const previousPeriodEndStr=formatDate(previousPeriodEnd),previousPeriodStart=new Date(previousPeriodEnd);previousPeriodStart.setDate(previousPeriodEnd.getDate()-_daysRange);const previousPeriodStartStr=formatDate(previousPeriodStart),currentPeriodCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).where("created_at",">=",currentPeriodStartStr).where("created_at","<=",todayStr).executeTakeFirst(),previousPeriodCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).where("created_at",">=",previousPeriodStartStr).where("created_at","<=",previousPeriodEndStr).executeTakeFirst(),currentCount=Number(currentPeriodCategories?.count||0),previousCount=Number(previousPeriodCategories?.count||0),difference=currentCount-previousCount,percentageChange=previousCount!==0?difference/previousCount*100:currentCount>0?100:0;return{current_period:currentCount,previous_period:previousCount,difference,percentage_change:percentageChange,days_range:_daysRange}}export async function fetchCategoryTree(){const allCategories=await fetchAll(),categoryMap=new Map;allCategories.forEach((category)=>{categoryMap.set(category.id,{...category,children:[]})});const rootCategories=[];allCategories.forEach((category)=>{const categoryWithChildren=categoryMap.get(category.id);if(category.parent_category_id){const parent=categoryMap.get(category.parent_category_id);if(parent)parent.children.push(categoryWithChildren)}else rootCategories.push(categoryWithChildren)});rootCategories.sort((a,b)=>a.display_order-b.display_order);const sortChildrenByDisplayOrder=(categories)=>{categories.sort((a,b)=>a.display_order-b.display_order);categories.forEach((category)=>{if(category.children.length>0)sortChildrenByDisplayOrder(category.children)})};sortChildrenByDisplayOrder(rootCategories);return rootCategories}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkRemove,deactivate,deactivateChildCategories,remove,removeChildCategories}from"./destroy";export{compareCategoryGrowth,fetchActive,fetchAll,fetchByDisplayOrder,fetchById,fetchByName,fetchCategoryTree,fetchChildCategories,fetchRootCategories,fetchStats}from"./fetch";export{findOrCreateByName,store}from"./store";export{update,updateActiveStatus,updateDisplayOrder,updateParent}from"./update";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{slug}from"@stacksjs/strings";import{fetchById}from"./fetch";export async function store(data){try{const parentCategoryId=data.parent_category_id?Number(data.parent_category_id):void 0;if(parentCategoryId!==void 0&&(!Number.isSafeInteger(parentCategoryId)||parentCategoryId<=0))throw new HttpError(422,"Parent category ID must be a positive integer");if(parentCategoryId&&!await fetchById(parentCategoryId))throw new HttpError(422,`Parent category with ID ${data.parent_category_id} not found`);const categoryData={...data,uuid:randomUUIDv7(),is_active:data.is_active??!0,parent_category_id:data.parent_category_id||null},result=await db.insertInto("categories").values(categoryData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create category");return result}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A category with this name or slug already exists");if(error instanceof Error)throw Error(`Failed to create category: ${error.message}`);throw error}}export async function findOrCreateByName(data){if(!data.name)throw Error("Name is required");const existingCategory=await db.selectFrom("categories").selectAll().where("name","=",data.name).executeTakeFirst();if(existingCategory)return existingCategory;const categoryData={name:data.name,slug:slug(data.name),is_active:data.is_active??!0,display_order:0};return await store(categoryData)}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{fetchById}from"./fetch";export async function update(id,data){if(!await fetchById(id))return;if(Object.hasOwn(data,"parent_category_id")){const parentCategoryId=data.parent_category_id?Number(data.parent_category_id):void 0;if(parentCategoryId!==void 0&&(!Number.isSafeInteger(parentCategoryId)||parentCategoryId<=0))throw new HttpError(422,"Parent category ID must be a positive integer");if(parentCategoryId===id)throw new HttpError(422,"A category cannot be its own parent");if(parentCategoryId&&!await fetchById(parentCategoryId))throw new HttpError(422,`Parent category with ID ${data.parent_category_id} not found`);if(parentCategoryId&&await wouldCreateCircularReference(id,String(parentCategoryId)))throw new HttpError(422,"This operation would create a circular category hierarchy")}try{const parentUpdate=Object.hasOwn(data,"parent_category_id")?{parent_category_id:data.parent_category_id||null}:{},result=await db.updateTable("categories").set({...data,...parentUpdate,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return result}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A category with this name or slug already exists");if(error instanceof Error)throw Error(`Failed to update category: ${error.message}`);throw error}}export async function updateDisplayOrder(id,newOrder){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);try{await db.updateTable("categories").set({display_order:newOrder,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category display order: ${error.message}`);throw error}}export async function updateActiveStatus(id,isActive){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);try{await db.updateTable("categories").set({is_active:isActive,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category active status: ${error.message}`);throw error}}export async function updateParent(id,newParentId){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);if(newParentId){const newParentIdNum=Number(newParentId);if(newParentIdNum===id)throw Error("A category cannot be its own parent");if(!await fetchById(newParentIdNum))throw Error(`Parent category with ID ${newParentId} not found`);if(await wouldCreateCircularReference(id,newParentId))throw Error("This operation would create a circular reference in the category hierarchy")}try{const updateObject={updated_at:formatDate(new Date)};if(newParentId===null)await db.updateTable("categories").set({...updateObject,parent_category_id:null}).where("id","=",id).execute();else await db.updateTable("categories").set({...updateObject,parent_category_id:newParentId}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category parent: ${error.message}`);throw error}}async function wouldCreateCircularReference(categoryId,newParentId){let currentParentId=Number(newParentId);const visited=new Set;while(currentParentId){if(visited.has(currentParentId))return!0;if(currentParentId===categoryId)return!0;visited.add(currentParentId);const parent=await fetchById(currentParentId),nextParentId=parent?.parent_category_id;if(!parent||!nextParentId)return!1;currentParentId=Number(nextParentId)}return!1}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import*as categories from"./categories";import*as items from"./items";import*as manufacturers from"./manufacturers";import*as reviews from"./reviews";import*as units from"./units";import*as variants from"./variants";export{categories,items,manufacturers,reviews,units,variants};const products={categories,items,manufacturers,reviews,units,variants};export default products;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{mutationCount}from"../../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("products").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete product item: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("products").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete product items: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,sql}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../../utils/model-row";export async function fetchById(id){const model=await db.selectFrom("products").where("id","=",id).selectAll().executeTakeFirst();if(model){let manufacturer=null,category=null;if(model.manufacturer_id)manufacturer=await db.selectFrom("manufacturers").where("id","=",model.manufacturer_id).selectAll().executeTakeFirst();if(model.category_id)category=await db.selectFrom("categories").where("id","=",model.category_id).selectAll().executeTakeFirst();return{...model,manufacturer,category}}return}export async function fetchAll(){const models=await db.selectFrom("products").selectAll().execute(),manufacturerIds=models.map((model)=>model.manufacturer_id).filter((id)=>id!==null&&id!==void 0),categoryIds=models.map((model)=>model.category_id).filter((id)=>id!==null&&id!==void 0);let manufacturersQuery=db.selectFrom("manufacturers"),categoriesQuery=db.selectFrom("categories");if(manufacturerIds.length>0)manufacturersQuery=manufacturersQuery.where("id","in",manufacturerIds);if(categoryIds.length>0)categoriesQuery=categoriesQuery.where("id","in",categoryIds);const allManufacturers=await manufacturersQuery.selectAll().execute(),allCategories=await categoriesQuery.selectAll().execute(),manufacturersById=allManufacturers.reduce((acc,manufacturer)=>{acc[manufacturer.id]=manufacturer;return acc},{}),categoriesById=allCategories.reduce((acc,category)=>{acc[category.id]=category;return acc},{});return models.map((model)=>({...model,manufacturer:model.manufacturer_id?manufacturersById[model.manufacturer_id]:null,category:model.category_id?categoriesById[model.category_id]:null}))}export async function getProductsByManufacturer(manufacturerId){try{const products=await db.selectFrom("products").selectAll().where("manufacturer_id","=",manufacturerId).orderBy("name").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by manufacturer: ${error.message}`);throw error}}export async function getProductsByCategory(categoryId){try{const products=await db.selectFrom("products").selectAll().where("category_id","=",categoryId).orderBy("name").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by category: ${error.message}`);throw error}}export async function getProductByUuid(uuid){try{const product=await db.selectFrom("products").selectAll().where("uuid","=",uuid).executeTakeFirst();return asModelRow(product,!0)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get product by UUID: ${error.message}`);throw error}}export async function formatProductOptions(){try{return(await db.selectFrom("products").select(["id","name","uuid","price"]).orderBy("name").execute()).filter((result)=>result.name!==null&&result.name!==void 0&&result.uuid!==null&&result.uuid!==void 0&&result.price!==null&&result.price!==void 0)}catch(error){if(error instanceof Error)throw TypeError(`Failed to format product options: ${error.message}`);throw error}}export async function getProductsByPriceRange(minPrice,maxPrice){try{const products=await db.selectFrom("products").selectAll().where("price",">=",minPrice).where("price","<=",maxPrice).orderBy("price").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by price range: ${error.message}`);throw error}}export async function fetchTopProducts(limit=5){try{return(await db.selectFrom("order_items").innerJoin("products","products.id","=","order_items.product_id").select(["products.id as id","products.name as name","products.inventory_count as inventory_count",db.fn.sum("order_items.quantity").as("units_sold"),sql`SUM(COALESCE(order_items.price, products.price) * order_items.quantity)`.as("revenue")]).groupBy(["products.id","products.name","products.inventory_count"]).orderBy("units_sold","desc").limit(limit).execute()).map((r)=>({id:Number(r.id),name:String(r.name??"Unnamed product"),units_sold:Number(r.units_sold??0),revenue:Number(r.revenue??0),inventory_count:Number(r.inventory_count??0)}))}catch(error){if(error instanceof Error)throw TypeError(`Failed to fetch top products: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,destroy}from"./destroy";export{fetchAll,fetchById,fetchTopProducts}from"./fetch";export{bulkStore,store}from"./store";export{update,updateAvailability,updateInventory,restock}from"./update";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{insertedId}from"../../utils/inserted-id";import{fetchById,getProductByUuid}from"./fetch";export async function store(data){try{const itemData={...data,uuid:randomUUIDv7()},result=await db.insertInto("products").values(itemData).executeTakeFirst();if(!result)throw Error("Failed to create product item");const id=insertedId(result),model=id!==void 0?await fetchById(id):await getProductByUuid(itemData.uuid);if(!model)throw Error("Failed to create product item");return model}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product item: ${error.message}`);throw error}}export async function bulkStore(data){if(!data.length)return 0;let createdCount=0;try{await db.transaction(async(trx)=>{for(const item of data){const itemData={...item,uuid:randomUUIDv7()};await trx.insertInto("products").values(itemData).execute();createdCount++}});return createdCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product items in bulk: ${error.message}`);throw error}}
|
|
@@ -70,6 +70,8 @@ export declare function adjustInventoryMany(updates: ReadonlyArray<{ id: number,
|
|
|
70
70
|
* @returns The updated product item
|
|
71
71
|
*/
|
|
72
72
|
export declare function updateInventory(id: number, inventoryCount?: number): Promise<ProductJsonResponse | undefined>;
|
|
73
|
+
/** Atomically add a delivery to the current inventory and return the committed quantity. */
|
|
74
|
+
export declare function restock(id: number, units: number): Promise<ProductJsonResponse | undefined>;
|
|
73
75
|
declare type ProductJsonResponse = ModelRow<typeof Product>;
|
|
74
76
|
declare type ProductUpdate = UpdateModelData<typeof Product>;
|
|
75
77
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,matchedRows}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";import{parseNumberInput,parsePositiveId}from"@stacksjs/validation/input";import{restockedQuantity}from"../../inventory";import{formatDate,withLockedRow}from"@stacksjs/orm";import{adjustInventoryOnConnection}from"../../utils/inventory-adjustment";import{fetchById}from"./fetch";export async function update(id,data){try{if(!id)throw Error("Product item ID is required for update");const result=await db.updateTable("products").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product item: ${error.message}`);throw error}}export async function bulkUpdate(data){if(!data.length)return 0;let updatedCount=0;try{await db.transaction(async(trx)=>{for(const item of data){if(!item.id)continue;if((await matchedRows(trx.updateTable("products").set({...item,updated_at:formatDate(new Date)}).where("id","=",item.id))).length>0)updatedCount++}});return updatedCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product items in bulk: ${error.message}`);throw error}}export async function updateAvailability(id,isAvailable){if(!await fetchById(id))throw Error(`Product item with ID ${id} not found`);try{await db.updateTable("products").set({is_available:isAvailable,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product item availability: ${error.message}`);throw error}}export async function adjustInventory(id,delta){if(!Number.isFinite(delta)||delta===0)throw Error("[commerce/inventory] adjustInventory delta must be a non-zero finite number");if(!await adjustInventoryOnConnection(db,id,delta,formatDate(new Date)))return null;return await fetchById(id)??null}export async function adjustInventoryMany(updates){if(updates.length===0)return{ok:!0,products:[]};try{return{ok:!0,products:await db.transaction(async(trx)=>{const out=[];for(let i=0;i<updates.length;i++){const{id,delta}=updates[i];if(!Number.isFinite(delta)||delta===0)throw Error("[commerce/inventory] adjustInventoryMany delta must be a non-zero finite number");if(!await adjustInventoryOnConnection(trx,id,delta,formatDate(new Date))){const reason=await trx.selectFrom("products").where("id","=",id).select("id").executeTakeFirst()?"out-of-stock":"not-found";throw Object.assign(Error(`adjustInventoryMany failed at index ${i}: ${reason}`),{__batchFail:!0,failedAt:i,productId:id,reason})}const refreshed=await trx.selectFrom("products").where("id","=",id).selectAll().executeTakeFirst();if(refreshed)out.push(refreshed)}return out})}}catch(err){if(err?.__batchFail)return{ok:!1,failedAt:err.failedAt,productId:err.productId,reason:err.reason};throw err}}export async function updateInventory(id,inventoryCount){const productId=parsePositiveId(id),quantity=inventoryCount===void 0?void 0:parseNumberInput(inventoryCount,"inventoryCount",0,2147483647,!0),product=await withLockedRow("products",{id:productId},async(row,tx)=>{if(quantity===void 0)return asModelRow(row);await tx.updateTable("products").set({inventory_count:quantity,updated_at:formatDate(new Date)}).where("id","=",productId).execute();const updated=await tx.selectFrom("products").selectAll().where("id","=",productId).executeTakeFirst();if(!updated)throw Error("Updated product disappeared");return asModelRow(updated)});if(!product)throw Error(`Product item with ID ${id} not found`);return product}export async function restock(id,units){const productId=parsePositiveId(id);return await withLockedRow("products",{id:productId},async(row,tx)=>{const quantity=restockedQuantity(row.inventory_count,units);await tx.updateTable("products").set({inventory_count:quantity,updated_at:formatDate(new Date)}).where("id","=",productId).execute();const updated=await tx.selectFrom("products").selectAll().where("id","=",productId).executeTakeFirst();if(!updated)throw Error("Restocked product disappeared");return asModelRow(updated)})??void 0}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{mutationCount}from"../../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("manufacturers").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete order: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("manufacturers").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete orders: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../../utils/model-row";export async function fetchAll(){return asModelRows(await db.selectFrom("manufacturers").selectAll().execute())}export async function fetchById(id){const row=await db.selectFrom("manufacturers").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(row,!0)}export async function fetchByUuid(uuid){const row=await db.selectFrom("manufacturers").where("uuid","=",uuid).selectAll().executeTakeFirst();return asModelRow(row,!0)}export async function fetchFeatured(limit=10){const rows=await db.selectFrom("manufacturers").where("featured","=",!0).selectAll().orderBy("manufacturer","asc").limit(limit).execute();return asModelRows(rows)}export async function fetchByCountry(country,options={}){const page=options.page||1,limit=options.limit||2,countResult=await db.selectFrom("manufacturers").select(db.fn.count("id").as("total")).where("country","=",country).executeTakeFirst(),total=Number(countResult?.total||0),manufacturers=await db.selectFrom("manufacturers").selectAll().where("country","=",country).limit(limit).offset((page-1)*limit).execute(),totalPages=Math.ceil(total/limit);return{data:asModelRows(manufacturers),paging:{total_records:total,page,total_pages:totalPages},next_cursor:page<totalPages?page+1:null}}export async function fetchWithProductCount(options={}){let query=db.selectFrom("manufacturers as m").leftJoin("products as p","p.manufacturer_id","=","m.id");if(options.country)query=query.where("m.country","=",options.country);if(options.featured!==void 0)query=query.where("m.featured","=",options.featured);query=query.select(["m.id","m.uuid","m.manufacturer","m.description","m.country","m.featured","m.created_at","m.updated_at",db.fn.count("p.id").as("product_count")]).groupBy("m.id");return query.execute()}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,destroy}from"./destroy";export{fetchAll,fetchByCountry,fetchById,fetchByUuid,fetchFeatured,fetchWithProductCount}from"./fetch";export{bulkStore,store}from"./store";export{update,updateByUuid,updateFeaturedStatus}from"./update";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";export async function store(data){try{const manufacturerData={...data,uuid:randomUUIDv7(),featured:data.featured??!1},result=await db.insertInto("manufacturers").values(manufacturerData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create manufacturer");return asModelRow(result)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A manufacturer with this name already exists");if(error instanceof Error)throw Error(`Failed to create manufacturer: ${error.message}`);throw error}}export async function bulkStore(data){if(!data.length)return 0;let createdCount=0;try{for(const manufacturer of data){const manufacturerData={...manufacturer,uuid:randomUUIDv7(),featured:manufacturer.featured??!1};await db.insertInto("manufacturers").values(manufacturerData).execute();createdCount++}return createdCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to create manufacturers in bulk: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";import{HttpError}from"@stacksjs/error-handling";import{formatDate,isUniqueViolation}from"@stacksjs/orm";export async function update(id,data){try{if(!id)throw Error("Manufacturer ID is required for update");const result=await db.updateTable("manufacturers").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return asModelRow(result)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A manufacturer with this name already exists");if(error instanceof Error)throw Error(`Failed to update manufacturer: ${error.message}`);throw error}}export async function updateFeaturedStatus(id,featured){try{const result=await db.updateTable("manufacturers").set({featured,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)throw Error("Failed to update manufacturer featured status");return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update manufacturer featured status: ${error.message}`);throw error}}export async function updateByUuid(uuid,data){try{const result=await db.updateTable("manufacturers").set({...data,updated_at:formatDate(new Date)}).where("uuid","=",uuid).returningAll().executeTakeFirst();if(!result)throw Error("Failed to update manufacturer");return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update manufacturer: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{mutationCount}from"../../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("reviews").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete review: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("reviews").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete reviews: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../../utils/model-row";export async function fetchById(id){return asModelRow(await db.selectFrom("reviews").where("id","=",id).selectAll().executeTakeFirst(),!0)}export async function fetchAll(){return asModelRows(await db.selectFrom("reviews").selectAll().execute())}export async function fetchByProductId(productId){return asModelRows(await db.selectFrom("reviews").where("product_id","=",productId).selectAll().execute())}export async function fetchByUserId(userId){return asModelRows(await db.selectFrom("reviews").where("customer_id","=",userId).selectAll().execute())}export async function fetchApprovedByProductId(productId){return asModelRows(await db.selectFrom("reviews").where("product_id","=",productId).where("is_approved","=",!0).selectAll().execute())}export async function fetchMostHelpfulByProductId(productId){return asModelRows(await db.selectFrom("reviews").where("product_id","=",productId).orderBy("helpful_votes","desc").selectAll().execute())}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,destroy}from"./destroy";export{fetchAll,fetchApprovedByProductId,fetchById,fetchByProductId,fetchByUserId,fetchMostHelpfulByProductId}from"./fetch";export{fetchStats,fetchStatsByProductIds}from"./stats";export{store}from"./store";export{update,updateVotes}from"./update";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";function emptyStats(){return{total:0,rated:0,commentsOnly:0,average:0,distribution:{1:0,2:0,3:0,4:0,5:0}}}function summarise(rows){const stats=emptyStats();stats.total=rows.length;let sum=0;for(const row of rows){if(row.rating===null||row.rating===void 0){stats.commentsOnly++;continue}const rating=Number(row.rating);if(Number.isNaN(rating)){stats.commentsOnly++;continue}stats.rated++;sum+=rating;const star=Math.min(5,Math.max(1,Math.round(rating)));stats.distribution[star]++}if(stats.rated>0)stats.average=Math.round(sum/stats.rated*10)/10;return stats}export async function fetchStats(productId){const rows=await db.selectFrom("reviews").select(["rating"]).where("product_id","=",productId).where("is_approved","=",!0).execute();return summarise(rows)}export async function fetchStatsByProductIds(productIds){const stats=new Map;for(const id of productIds)stats.set(id,emptyStats());if(productIds.length===0)return stats;const rows=await db.selectFrom("reviews").select(["product_id","rating"]).where("product_id","in",productIds).where("is_approved","=",!0).execute(),grouped=new Map;for(const row of rows){const bucket=grouped.get(row.product_id);if(bucket)bucket.push({rating:row.rating});else grouped.set(row.product_id,[{rating:row.rating}])}for(const[productId,bucket]of grouped)stats.set(productId,summarise(bucket));return stats}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";function isEmptyReview(data){const hasRating=data.rating!==void 0&&data.rating!==null,hasContent=typeof data.content==="string"&&data.content.trim().length>0,hasTitle=typeof data.title==="string"&&data.title.trim().length>0;return!hasRating&&!hasContent&&!hasTitle}export async function store(data){try{if(isEmptyReview(data))throw Error("A review needs a rating or something written: both were empty");const reviewData={...data,uuid:randomUUIDv7(),rating:data.rating??null,is_verified_purchase:data.is_verified_purchase??!1,is_approved:data.is_approved??!1,helpful_votes:data.helpful_votes??0,unhelpful_votes:data.unhelpful_votes??0,is_featured:data.is_featured??!1},result=await db.insertInto("reviews").values(reviewData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create review");return asModelRow(result)}catch(error){if(error instanceof Error){if(error.message.includes("Duplicate entry"))throw Error("A review with this code already exists");throw Error(`Failed to create review: ${error.message}`)}throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,sqlHelpers}from"@stacksjs/database/runtime";import{env}from"@stacksjs/env";import{asModelRow}from"../../utils/model-row";import{formatDate}from"@stacksjs/orm";export async function update(id,data){try{if(!id)throw Error("Review ID is required for update");const result=await db.updateTable("reviews").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update review: ${error.message}`);throw error}}export async function updateVotes(id,voteType,increment=!0){try{const column=voteType==="helpful"?"helpful_votes":"unhelpful_votes",expr=increment?`COALESCE("${column}", 0) + 1`:`MAX(COALESCE("${column}", 0) - 1, 0)`,{param}=sqlHelpers(env.DB_CONNECTION||"sqlite"),result=await db.unsafe(`UPDATE "reviews" SET "${column}" = ${expr}, "updated_at" = ${param(1)} WHERE "id" = ${param(2)} RETURNING *`,[formatDate(new Date),id]).execute(),row=Array.isArray(result)?result[0]:result;if(!row)throw Error("Failed to update review votes");return asModelRow(row)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update review votes: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{mutationCount}from"../../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("product_units").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete product unit: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("product_units").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete product units: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../../utils/model-row";export async function fetchById(id){const row=await db.selectFrom("product_units").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(row,!0)}export async function fetchAll(){return asModelRows(await db.selectFrom("product_units").selectAll().execute())}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,destroy}from"./destroy";export{fetchAll,fetchById}from"./fetch";export{bulkStore,formatUnitOptions,getDefaultUnit,store}from"./store";export{bulkUpdate,update,updateDefaultStatus}from"./update";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";export async function store(data){try{return await db.transaction(async(trx)=>{const unitData={...data,uuid:randomUUIDv7()},result=await trx.insertInto("product_units").values(unitData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create product unit");if(unitData.is_default&&unitData.type)await trx.updateTable("product_units").set({is_default:!1}).where("type","=",unitData.type).where("id","!=",result.id).execute();return asModelRow(result)})}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product unit: ${error.message}`);throw error}}export async function bulkStore(data){if(!data.length)return 0;let createdCount=0;try{for(const unit of data){const unitData={...unit,uuid:randomUUIDv7()},result=await db.insertInto("product_units").values(unitData).returningAll().executeTakeFirst();if(unitData.is_default&&unitData.type&&result)await db.updateTable("product_units").set({is_default:!1}).where("type","=",unitData.type).where("id","!=",result.id).execute();createdCount++}return createdCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product units in bulk: ${error.message}`);throw error}}export function formatUnitOptions(type){try{let query=db.selectFrom("product_units").select(["id","name","abbreviation","is_default"]);if(type)query=query.where("type","=",type);query=query.orderBy("name");return query.execute().then((results)=>results.map((result)=>({id:String(result.id),name:result.name,abbreviation:result.abbreviation,is_default:result.is_default})))}catch(error){if(error instanceof Error)throw TypeError(`Failed to format unit options: ${error.message}`);throw error}}export async function getDefaultUnit(type){try{const defaultUnit=await db.selectFrom("product_units").selectAll().where("type","=",type).where("is_default","=",!0).executeTakeFirst();return asModelRow(defaultUnit,!0)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get default unit: ${error.message}`);throw error}}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,matchedRows}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";import{formatDate}from"@stacksjs/orm";export async function update(id,data){try{if(!id)throw Error("Product unit ID is required for update");return await db.transaction(async(trx)=>{const current=await trx.selectFrom("product_units").select(["type","is_default"]).where("id","=",id).executeTakeFirst();if(!current)return;const unitType=data.type??current.type;if((data.is_default??Boolean(current.is_default))&&unitType)await trx.updateTable("product_units").set({is_default:!1,updated_at:formatDate(new Date)}).where("type","=",unitType).where("id","!=",id).execute();const result=await trx.updateTable("product_units").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return asModelRow(result)})}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product unit: ${error.message}`);throw error}}export async function bulkUpdate(data){if(!data.length)return 0;let updatedCount=0;try{for(const unit of data){const unitRecord=unit;if(!unitRecord.id)continue;const updateFields=unitRecord.data&&typeof unitRecord.data==="object"?unitRecord.data:{...unitRecord};delete updateFields.id;const matched=await matchedRows(db.updateTable("product_units").set({...updateFields,updated_at:formatDate(new Date)}).where("id","=",unitRecord.id)),fields=updateFields;if(fields.is_default===!0&&fields.type)await db.updateTable("product_units").set({is_default:!1}).where("type","=",fields.type).where("id","!=",unitRecord.id).execute();if(matched.length>0)updatedCount++}return updatedCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product units in bulk: ${error.message}`);throw error}}export async function updateDefaultStatus(id,isDefault){try{return await db.transaction(async(trx)=>{const unit=await trx.selectFrom("product_units").select("type").where("id","=",id).executeTakeFirst();if(!unit)return!1;const updatedAt=formatDate(new Date);if(isDefault&&unit.type)await trx.updateTable("product_units").set({is_default:!1,updated_at:updatedAt}).where("type","=",unit.type).where("id","!=",id).execute();await trx.updateTable("product_units").set({is_default:isDefault,updated_at:updatedAt}).where("id","=",id).execute();return!0})}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product unit default status: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{mutationCount}from"../../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("product_variants").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete product variant: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("product_variants").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete product variants: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../../utils/model-row";export async function fetchById(id){const row=await db.selectFrom("product_variants").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(row,!0)}export async function fetchAll(){return asModelRows(await db.selectFrom("product_variants").selectAll().execute())}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,destroy}from"./destroy";export{fetchAll,fetchById}from"./fetch";export{bulkStore,formatVariantOptions,generateVariantCombinations,store}from"./store";export{bulkUpdate,update}from"./update";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";export async function store(data){try{const variantData={...data,uuid:randomUUIDv7()},result=await db.insertInto("product_variants").values(variantData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create product variant");return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product variant: ${error.message}`);throw error}}export async function bulkStore(data){if(!data.length)return 0;let createdCount=0;try{for(const variant of data){const variantData={...variant,uuid:randomUUIDv7()};await db.insertInto("product_variants").values(variantData).execute();createdCount++}return createdCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product variants in bulk: ${error.message}`);throw error}}export function formatVariantOptions(rawOptions){if(!rawOptions||!rawOptions.length)return JSON.stringify([]);const cleanedOptions=rawOptions.map((option)=>option.trim()).filter(Boolean);return JSON.stringify(cleanedOptions)}export function generateVariantCombinations(optionSets){const keys=Object.keys(optionSets);if(!keys.length)return[];function createCombinations(currentIndex,currentCombination){if(currentIndex===keys.length)return[currentCombination];const currentKey=keys[currentIndex],currentOptions=optionSets[currentKey]??[],combinations=[];for(const option of currentOptions){const newCombination={...currentCombination,[currentKey]:option};combinations.push(...createCombinations(currentIndex+1,newCombination))}return combinations}return createCombinations(0,{})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db,matchedRows}from"@stacksjs/database/runtime";import{asModelRow}from"../../utils/model-row";import{formatDate}from"@stacksjs/orm";export async function update(id,data){try{const result=await db.updateTable("product_variants").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product variant: ${error.message}`);throw error}}export async function bulkUpdate(data){if(!data.length)return 0;let updatedCount=0;try{for(const variant of data){if(!variant.id)continue;if((await matchedRows(db.updateTable("product_variants").set({...variant,updated_at:formatDate(new Date)}).where("id","=",variant.id))).length>0)updatedCount++}return updatedCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product variants in bulk: ${error.message}`);throw error}}export async function updateStatus(id,status){try{return(await matchedRows(db.updateTable("product_variants").set({status,updated_at:formatDate(new Date)}).where("id","=",id))).length>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to update product variant status: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{mutationCount}from"../utils/mutation-count";export async function destroy(id){try{const result=await db.deleteFrom("receipts").where("id","=",id).executeTakeFirst();return mutationCount(result)>0}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete print log: ${error.message}`);throw error}}export async function bulkDestroy(ids){if(!ids.length)return 0;try{const result=await db.deleteFrom("receipts").where("id","in",ids).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to delete receipts: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../utils/model-row";import{formatDate}from"@stacksjs/orm";import{aggregateStats}from"../utils/typed-stats";function dateBoundary(value){return formatDate(value)}function receiptDate(value){if(value instanceof Date)return Number.isNaN(value.getTime())?null:value;if(typeof value==="number"){const parsed=new Date(value<=2147483647?value*1000:value);return Number.isNaN(parsed.getTime())?null:parsed}const raw=String(value||"").trim();if(!raw)return null;if(/^\d{10}$/.test(raw)){const parsed=new Date(Number(raw)*1000);return Number.isNaN(parsed.getTime())?null:parsed}const parsed=new Date(raw.includes("T")?raw:`${raw.replace(" ","T")}Z`);return Number.isNaN(parsed.getTime())?null:parsed}export async function fetchById(id){const row=await db.selectFrom("receipts").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(row,!0)}export async function fetchAll(){return asModelRows(await db.selectFrom("receipts").selectAll().execute())}export async function fetchPrintJobStats(_startDate,_endDate){const start=dateBoundary(_startDate),end=dateBoundary(_endDate),stats=await aggregateStats("receipts",{total:{kind:"count"},success:{kind:"count",filter:{column:"status",op:"=",value:"success"}},failed:{kind:"count",filter:{column:"status",op:"=",value:"failed"}},warning:{kind:"count",filter:{column:"status",op:"=",value:"warning"}},averageSize:{kind:"avg",column:"size"},averagePages:{kind:"avg",column:"pages"},averageDuration:{kind:"avg",column:"duration"}},(qb)=>qb.where("timestamp",">=",start).where("timestamp","<=",end));return{total:stats.total,success:stats.success,failed:stats.failed,warning:stats.warning,averageSize:Math.round(stats.averageSize),averagePages:Math.round(stats.averagePages),averageDuration:Math.round(stats.averageDuration)}}export async function fetchSuccessRate(_startDate,_endDate){const start=dateBoundary(_startDate),end=dateBoundary(_endDate),stats=await aggregateStats("receipts",{total:{kind:"count"},success:{kind:"count",filter:{column:"status",op:"=",value:"success"}},failed:{kind:"count",filter:{column:"status",op:"=",value:"failed"}},warning:{kind:"count",filter:{column:"status",op:"=",value:"warning"}}},(qb)=>qb.where("timestamp",">=",start).where("timestamp","<=",end));return{successRate:stats.total>0?Math.round(stats.success/stats.total*100):0,total:stats.total,success:stats.success,failed:stats.failed,warning:stats.warning}}export async function fetchPageStats(_startDate,_endDate){const start=dateBoundary(_startDate),end=dateBoundary(_endDate),stats=await aggregateStats("receipts",{totalReceipts:{kind:"count"},totalPages:{kind:"sum",column:"pages"},averagePagesPerReceipt:{kind:"avg",column:"pages"}},(qb)=>qb.where("timestamp",">=",start).where("timestamp","<=",end));return{totalPages:stats.totalPages,averagePagesPerReceipt:Math.round(stats.averagePagesPerReceipt),totalReceipts:stats.totalReceipts}}export async function fetchPrintTimeStats(_startDate,_endDate){const start=dateBoundary(_startDate),end=dateBoundary(_endDate),stats=await aggregateStats("receipts",{totalJobs:{kind:"count"},averageDuration:{kind:"avg",column:"duration"},minDuration:{kind:"min",column:"duration"},maxDuration:{kind:"max",column:"duration"}},(qb)=>qb.where("timestamp",">=",start).where("timestamp","<=",end));return{averageDuration:Math.round(stats.averageDuration),minDuration:stats.minDuration,maxDuration:stats.maxDuration,totalJobs:stats.totalJobs}}export async function fetchPrintsPerHour(_startDate,_endDate){const startMs=typeof _startDate==="number"?_startDate:_startDate.getTime(),endMs=typeof _endDate==="number"?_endDate:_endDate.getTime(),start=dateBoundary(_startDate),end=dateBoundary(_endDate),receipts=await db.selectFrom("receipts").where("timestamp",">=",start).where("timestamp","<=",end).selectAll().execute(),totalPrints=receipts.length,totalHours=Math.ceil((endMs-startMs)/3600000),printsPerHour=totalHours>0?Math.round(totalPrints/totalHours):0,hourlyBreakdown=Array.from({length:24},(_,i)=>({hour:i,count:0}));receipts.forEach((receipt)=>{const date=receiptDate(receipt.timestamp);if(!date)return;hourlyBreakdown[date.getHours()].count++});return{totalPrints,totalHours,printsPerHour,hourlyBreakdown}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{bulkDestroy,destroy}from"./destroy";export{fetchAll,fetchById,fetchPageStats,fetchPrintJobStats,fetchPrintsPerHour,fetchPrintTimeStats,fetchSuccessRate}from"./fetch";export{bulkStore,store}from"./store";export{update,updatePrintJob,updateStatus}from"./update";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Export the TSP IV printer instance
|
|
2
|
+
export declare const tspIVPrinter: PrinterDriver;
|
|
3
|
+
declare interface Printer {
|
|
4
|
+
id: string | number
|
|
5
|
+
name: string
|
|
6
|
+
status: string
|
|
7
|
+
}
|
|
8
|
+
declare interface PrintJob {
|
|
9
|
+
printer: Printer
|
|
10
|
+
receipt: Receipt
|
|
11
|
+
}
|
|
12
|
+
declare interface PrinterDriver {
|
|
13
|
+
print: (printJob: PrintJob) => Promise<void>
|
|
14
|
+
cleanUp: (printer: Printer) => Promise<void>
|
|
15
|
+
checkStatus: (printer: Printer) => Promise<boolean>
|
|
16
|
+
findPrinters: () => Promise<Printer[]>
|
|
17
|
+
setup: (printer: Printer) => Promise<void>
|
|
18
|
+
restart: () => Promise<void>
|
|
19
|
+
canInteractWithPrinter: (printer: Printer) => Promise<boolean>
|
|
20
|
+
}
|
|
21
|
+
declare interface Receipt {
|
|
22
|
+
id: string | number
|
|
23
|
+
name: string
|
|
24
|
+
price: number
|
|
25
|
+
quantity: number
|
|
26
|
+
template: Path
|
|
27
|
+
}
|
|
28
|
+
declare type Path = string;
|
|
29
|
+
declare class TSPIVPrinter implements PrinterDriver {
|
|
30
|
+
print(printJob: PrintJob): Promise<void>;
|
|
31
|
+
cleanUp(_printer: Printer): Promise<void>;
|
|
32
|
+
checkStatus(_printer: Printer): Promise<boolean>;
|
|
33
|
+
findPrinters(): Promise<Printer[]>;
|
|
34
|
+
setup(_printer: Printer): Promise<void>;
|
|
35
|
+
restart(): Promise<void>;
|
|
36
|
+
canInteractWithPrinter(_printer: Printer): Promise<boolean>;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log}from"@stacksjs/logging";class TSPIVPrinter{async print(printJob){log.debug("TSP IV Printer: Printing receipt",printJob.receipt)}async cleanUp(_printer){log.debug("TSP IV Printer: Cleaning up print job...")}async checkStatus(_printer){log.debug("TSP IV Printer: Checking online status...");return!0}async findPrinters(){log.debug("TSP IV Printer: Finding printers...");return[]}async setup(_printer){log.debug("TSP IV Printer: Setting up printer...")}async restart(){log.debug("TSP IV Printer: Restarting...")}async canInteractWithPrinter(_printer){log.debug("TSP IV Printer: Checking if can interact with printer...");return!0}}export const tspIVPrinter=new TSPIVPrinter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../utils/model-row";import{mutationCount}from"../utils/mutation-count";import{receiptTimestamp}from"./timestamp";export async function store(data){try{const uuid=randomUUIDv7(),receiptData={...data,timestamp:receiptTimestamp(data.timestamp),uuid};await db.insertInto("receipts").values(receiptData).executeTakeFirst();const result=await db.selectFrom("receipts").where("uuid","=",uuid).selectAll().executeTakeFirst();if(!result)throw Error("Failed to create receipt");return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to create receipt: ${error.message}`);throw error}}export async function bulkStore(data){if(!data.length)return 0;try{const receiptDataArray=data.map((item)=>({...item,timestamp:receiptTimestamp(item.timestamp),uuid:randomUUIDv7()})),result=await db.insertInto("receipts").values(receiptDataArray).executeTakeFirst();return mutationCount(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to create receipts in bulk: ${error.message}`);throw error}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{formatDate}from"@stacksjs/orm";export function receiptTimestamp(value){try{if(typeof value==="number")return formatDate(value<=2147483647?value*1000:value);if(typeof value==="string"&&/^\d{10}$/.test(value))return formatDate(Number(value)*1000);return formatDate(value)}catch{throw TypeError("Receipt timestamp must be a valid date")}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../utils/model-row";import{formatDate}from"@stacksjs/orm";import{fetchById}from"./fetch";import{receiptTimestamp}from"./timestamp";export async function update(id,data){try{if(!id)throw Error("Receipt ID is required for update");const updateData={...data,...data.timestamp===void 0?{}:{timestamp:receiptTimestamp(data.timestamp)},updated_at:formatDate(new Date)};await db.updateTable("receipts").set(updateData).where("id","=",id).execute();const result=await fetchById(id);if(!result)return;return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update receipt: ${error.message}`);throw error}}export async function updateStatus(id,status){try{await db.updateTable("receipts").set({status,updated_at:formatDate(new Date)}).where("id","=",id).execute();const result=await fetchById(id);if(!result)throw Error("Failed to update receipt status");return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update receipt status: ${error.message}`);throw error}}export async function updatePrintJob(id,size,pages,duration){try{const updateData={updated_at:formatDate(new Date)};if(size!==void 0)updateData.size=size;if(pages!==void 0)updateData.pages=pages;if(duration!==void 0)updateData.duration=duration;await db.updateTable("receipts").set(updateData).where("id","=",id).execute();const result=await fetchById(id);if(!result)throw Error("Failed to update receipt job information");return asModelRow(result)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update receipt job information: ${error.message}`);throw error}}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
function d(r,n,e){if(![r,n,e].every((b)=>Number.isSafeInteger(b)&&b>=0)||r<1||r>n+e)throw Error("Refund amount exceeds remaining refundable amount");let u=Math.min(r,n);return{cash:u,credit:r-u}}function f(r,n,e,u){if(![r,n,e,u].every((b)=>Number.isSafeInteger(b)&&b>=0)||r+n>e)throw Error("Invalid refund tax amounts");if(e===0)return 0;return Math.round((r+n)*u/e)-Math.round(r*u/e)}export{d as allocateRefund,f as refundTaxDelta};
|
|
1
|
+
export function allocateRefund(amount,cashRemaining,creditRemaining){if(![amount,cashRemaining,creditRemaining].every((value)=>Number.isSafeInteger(value)&&value>=0)||amount<1||amount>cashRemaining+creditRemaining)throw Error("Refund amount exceeds remaining refundable amount");const cash=Math.min(amount,cashRemaining);return{cash,credit:amount-cash}}export function refundTaxDelta(before,amount,total,tax){if(![before,amount,total,tax].every((value)=>Number.isSafeInteger(value)&&value>=0)||before+amount>total)throw Error("Invalid refund tax amounts");if(total===0)return 0;return Math.round((before+amount)*tax/total)-Math.round(before*tax/total)}
|
package/dist/register.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
var g=Object.defineProperty;var k=(t)=>t;function x(t,r){this[t]=k.bind(null,r)}var b=(t,r)=>{for(var e in r)g(t,e,{get:r[e],enumerable:!0,configurable:!0,set:x.bind(r,e)})};var R=(t,r,e)=>()=>{if(t)try{r=t(t=0)}catch(n){e=[n]}if(e)throw e[0];return r};var q={};b(q,{CENTS_FIELDS:()=>y,DEFAULT_FIELDS:()=>u,addToBasket:()=>L,basketCount:()=>w,basketLines:()=>p,basketTaxable:()=>F,basketTotal:()=>I,canAdd:()=>m,groupsOf:()=>h,normalizeBasket:()=>d,setQuantity:()=>B,shelfLeft:()=>l,stockLabel:()=>S});var u={id:"id",price:"price",inventory:"inventory",lowStock:"low_stock_threshold",taxable:"taxable"},y={id:"id",price:"price_cents",inventory:"inventory_quantity",lowStock:"low_stock_threshold",taxable:"taxable"};function a(t,r){return Number(t[r.id])}function f(t,r){let e=t[r.inventory];return e===null||e===void 0||e===""?null:Number(e)}function d(t){let r=new Map;for(let{productId:e,quantity:n}of t){if(!Number.isSafeInteger(e)||e<1||!Number.isSafeInteger(n)||n<1)throw Error("Basket products and quantities must be positive safe integers");let o=(r.get(e)||0)+n;if(!Number.isSafeInteger(o))throw Error("Basket quantity exceeds the safe integer range");r.set(e,o)}return[...r].map(([e,n])=>({productId:e,quantity:n}))}function l(t,r,e=u){let n=f(t,e);if(n===null)return null;let o=r.filter((i)=>i.productId===a(t,e)).reduce((i,c)=>i+c.quantity,0);return Math.max(0,n-o)}function m(t,r,e=u){let n=l(t,r,e);return n===null||n>0}function L(t,r,e=u){let n=d(t);if(!m(r,n,e))return t;t=n;let o=a(r,e);return t.some((i)=>i.productId===o)?t.map((i)=>i.productId===o?{...i,quantity:i.quantity+1}:i):[...t,{productId:o,quantity:1}]}function B(t,r,e,n=u){let o=a(r,n),i=f(r,n);t=d(t);let c=Math.min(Math.max(0,Math.floor(Number(e)||0)),i===null?Number.POSITIVE_INFINITY:i);if(c===0)return t.filter((s)=>s.productId!==o);return t.some((s)=>s.productId===o)?t.map((s)=>s.productId===o?{...s,quantity:c}:s):[...t,{productId:o,quantity:c}]}function p(t,r,e=u){return d(t).map((n)=>({...n,product:r.find((o)=>a(o,e)===n.productId)})).filter((n)=>!!n.product)}function I(t,r,e=u){return p(t,r,e).reduce((n,o)=>n+Number(o.product[e.price]||0)*o.quantity,0)}function F(t,r,e=u){return p(t,r,e).filter((n)=>n.product[e.taxable]!==!1).reduce((n,o)=>n+Number(o.product[e.price]||0)*o.quantity,0)}function w(t){return t.reduce((r,e)=>r+e.quantity,0)}function S(t,r=[],e=u,n=5){let o=l(t,r,e);if(o===null)return null;if(o===0)return{text:"Sold out",tone:"out"};let i=t[e.lowStock]===null||t[e.lowStock]===void 0?n:Number(t[e.lowStock]);return{text:`${o} left`,tone:o<=i?"low":"ok"}}function h(t,r){let e=[];for(let n of t){let o=r(n);if(!e.includes(o))e.push(o)}return e}export{y as CENTS_FIELDS,u as DEFAULT_FIELDS,L as addToBasket,w as basketCount,p as basketLines,F as basketTaxable,I as basketTotal,m as canAdd,h as groupsOf,d as normalizeBasket,B as setQuantity,l as shelfLeft,S as stockLabel};
|
|
1
|
+
export const DEFAULT_FIELDS={id:"id",price:"price",inventory:"inventory",lowStock:"low_stock_threshold",taxable:"taxable"},CENTS_FIELDS={id:"id",price:"price_cents",inventory:"inventory_quantity",lowStock:"low_stock_threshold",taxable:"taxable"};function idOf(product,fields){return Number(product[fields.id])}function stockOf(product,fields){const value=product[fields.inventory];return value===null||value===void 0||value===""?null:Number(value)}export function normalizeBasket(basket){const quantities=new Map;for(const{productId,quantity}of basket){if(!Number.isSafeInteger(productId)||productId<1||!Number.isSafeInteger(quantity)||quantity<1)throw Error("Basket products and quantities must be positive safe integers");const total=(quantities.get(productId)||0)+quantity;if(!Number.isSafeInteger(total))throw Error("Basket quantity exceeds the safe integer range");quantities.set(productId,total)}return[...quantities].map(([productId,quantity])=>({productId,quantity}))}export function shelfLeft(product,basket,fields=DEFAULT_FIELDS){const stock=stockOf(product,fields);if(stock===null)return null;const inBasket=basket.filter((line)=>line.productId===idOf(product,fields)).reduce((sum,line)=>sum+line.quantity,0);return Math.max(0,stock-inBasket)}export function canAdd(product,basket,fields=DEFAULT_FIELDS){const left=shelfLeft(product,basket,fields);return left===null||left>0}export function addToBasket(basket,product,fields=DEFAULT_FIELDS){const normalized=normalizeBasket(basket);if(!canAdd(product,normalized,fields))return basket;basket=normalized;const id=idOf(product,fields);return basket.some((line)=>line.productId===id)?basket.map((line)=>line.productId===id?{...line,quantity:line.quantity+1}:line):[...basket,{productId:id,quantity:1}]}export function setQuantity(basket,product,quantity,fields=DEFAULT_FIELDS){const id=idOf(product,fields),stock=stockOf(product,fields);basket=normalizeBasket(basket);const wanted=Math.min(Math.max(0,Math.floor(Number(quantity)||0)),stock===null?Number.POSITIVE_INFINITY:stock);if(wanted===0)return basket.filter((line)=>line.productId!==id);return basket.some((line)=>line.productId===id)?basket.map((line)=>line.productId===id?{...line,quantity:wanted}:line):[...basket,{productId:id,quantity:wanted}]}export function basketLines(basket,products,fields=DEFAULT_FIELDS){return normalizeBasket(basket).map((line)=>({...line,product:products.find((product)=>idOf(product,fields)===line.productId)})).filter((line)=>!!line.product)}export function basketTotal(basket,products,fields=DEFAULT_FIELDS){return basketLines(basket,products,fields).reduce((sum,line)=>sum+Number(line.product[fields.price]||0)*line.quantity,0)}export function basketTaxable(basket,products,fields=DEFAULT_FIELDS){return basketLines(basket,products,fields).filter((line)=>line.product[fields.taxable]!==!1).reduce((sum,line)=>sum+Number(line.product[fields.price]||0)*line.quantity,0)}export function basketCount(basket){return basket.reduce((sum,line)=>sum+line.quantity,0)}export function stockLabel(product,basket=[],fields=DEFAULT_FIELDS,defaultLow=5){const left=shelfLeft(product,basket,fields);if(left===null)return null;if(left===0)return{text:"Sold out",tone:"out"};const threshold=product[fields.lowStock]===null||product[fields.lowStock]===void 0?defaultLow:Number(product[fields.lowStock]);return{text:`${left} left`,tone:left<=threshold?"low":"ok"}}export function groupsOf(products,groupOf){const seen=[];for(const product of products){const name=groupOf(product);if(!seen.includes(name))seen.push(name)}return seen}
|
package/dist/releases.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
var _=Object.defineProperty;var b=(e)=>e;function h(e,t){this[e]=b.bind(null,t)}var v=(e,t)=>{for(var n in t)_(e,n,{get:t[n],enumerable:!0,configurable:!0,set:h.bind(t,n)})};var A=(e,t,n)=>()=>{if(e)try{t=e(e=0)}catch(o){n=[o]}if(n)throw n[0];return t};var p={};v(p,{RELEASE_MODES:()=>D,describeRollout:()=>S,isReleaseMode:()=>f,releaseSchedule:()=>k,rolloutFromProduct:()=>w,unlockAt:()=>y,unlockedPrefix:()=>R});var D=["all_at_once","drip","scheduled"];function f(e){return typeof e==="string"&&D.includes(e)}function w(e){let t=Number(e.release_interval_days),n=Number(e.release_initial_count),o=s(e.release_starts_at);return{mode:f(e.release_mode)?e.release_mode:"all_at_once",intervalDays:Number.isInteger(t)&&t>0?t:7,initialCount:Number.isInteger(n)&&n>0?n:1,startsAt:o===null?null:new Date(o).toISOString()}}function s(e){if(e==null||e==="")return null;let t=new Date(String(e)).getTime();return Number.isFinite(t)?t:null}function d(e,t){return Math.max(0,t-(e.initialCount-1))}function y(e,t,n,o={}){let i=s(e.startsAt),a=Math.max(n.getTime(),i??0);if(e.mode==="scheduled"){let r=s(o.releaseAt);if(r!==null)return new Date(r);let l=i??n.getTime();return new Date(l+d(e,t)*e.intervalDays*86400000)}if(e.mode==="drip"){let r=o.offsetDays;if(r!=null&&Number.isFinite(Number(r))&&Number(r)>=0)return new Date(a+Number(r)*86400000);return new Date(a+d(e,t)*e.intervalDays*86400000)}return new Date(a)}function k(e,t,n,o,i=[]){let a=[];for(let r=0;r<t;r++){let l=y(e,r,n,i[r]||{});a.push({index:r,unlock_at:l.toISOString(),unlocked:l.getTime()<=o.getTime()})}return a}function R(e){let t=0;for(let n of e){if(!n.unlocked)break;t++}return t}function S(e,t,n,o=new Date){let i=(c)=>`${c} ${n}${c===1?"":"s"}`,a=s(e.startsAt),r=a!==null&&a>o.getTime()?` from ${new Date(a).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"})}`:"";if(t<=0)return"";if(t===1)return`Available${r||" immediately"}.`;if(e.mode==="all_at_once"||t<=e.initialCount)return`All ${i(t)} available${r||" immediately"}.`;let l=e.intervalDays===1?"every day":e.intervalDays===7?"every week":`every ${e.intervalDays} days`,u=e.initialCount===1,m=u?`The first ${n}`:`The first ${i(e.initialCount)}`;if(e.mode==="drip")return`${m} ${u?"unlocks":"unlock"}${r||" on purchase"}, then one more ${l}.`;if(r)return`${m} ${u?"releases":"release"} on ${r.slice(6)}, then one more ${l}, on the same dates for everyone.`;return`Releasing on a shared schedule${a===null?"":` since ${new Date(a).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"})}`}, one more ${n} ${l}. Join now and everything already out is yours.`}export{D as RELEASE_MODES,S as describeRollout,f as isReleaseMode,k as releaseSchedule,w as rolloutFromProduct,y as unlockAt,R as unlockedPrefix};
|
|
1
|
+
export const RELEASE_MODES=["all_at_once","drip","scheduled"];const DAY_MS=86400000;export function isReleaseMode(value){return typeof value==="string"&&RELEASE_MODES.includes(value)}export function rolloutFromProduct(product){const interval=Number(product.release_interval_days),initial=Number(product.release_initial_count),startsAt=parseTime(product.release_starts_at);return{mode:isReleaseMode(product.release_mode)?product.release_mode:"all_at_once",intervalDays:Number.isInteger(interval)&&interval>0?interval:7,initialCount:Number.isInteger(initial)&&initial>0?initial:1,startsAt:startsAt===null?null:new Date(startsAt).toISOString()}}function parseTime(value){if(value==null||value==="")return null;const time=new Date(String(value)).getTime();return Number.isFinite(time)?time:null}function stepsFor(config,index){return Math.max(0,index-(config.initialCount-1))}export function unlockAt(config,index,purchasedAt,override={}){const launch=parseTime(config.startsAt),anchor=Math.max(purchasedAt.getTime(),launch??0);if(config.mode==="scheduled"){const fixed=parseTime(override.releaseAt);if(fixed!==null)return new Date(fixed);const base=launch??purchasedAt.getTime();return new Date(base+stepsFor(config,index)*config.intervalDays*DAY_MS)}if(config.mode==="drip"){const offset=override.offsetDays;if(offset!=null&&Number.isFinite(Number(offset))&&Number(offset)>=0)return new Date(anchor+Number(offset)*DAY_MS);return new Date(anchor+stepsFor(config,index)*config.intervalDays*DAY_MS)}return new Date(anchor)}export function releaseSchedule(config,count,purchasedAt,now,overrides=[]){const units=[];for(let index=0;index<count;index++){const at=unlockAt(config,index,purchasedAt,overrides[index]||{});units.push({index,unlock_at:at.toISOString(),unlocked:at.getTime()<=now.getTime()})}return units}export function unlockedPrefix(schedule){let count=0;for(const unit of schedule){if(!unit.unlocked)break;count++}return count}export function describeRollout(config,count,unit,now=new Date){const plural=(n)=>`${n} ${unit}${n===1?"":"s"}`,launch=parseTime(config.startsAt),launchText=launch!==null&&launch>now.getTime()?` from ${new Date(launch).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"})}`:"";if(count<=0)return"";if(count===1)return`Available${launchText||" immediately"}.`;if(config.mode==="all_at_once"||count<=config.initialCount)return`All ${plural(count)} available${launchText||" immediately"}.`;const every=config.intervalDays===1?"every day":config.intervalDays===7?"every week":`every ${config.intervalDays} days`,one=config.initialCount===1,start=one?`The first ${unit}`:`The first ${plural(config.initialCount)}`;if(config.mode==="drip")return`${start} ${one?"unlocks":"unlock"}${launchText||" on purchase"}, then one more ${every}.`;if(launchText)return`${start} ${one?"releases":"release"} on ${launchText.slice(6)}, then one more ${every}, on the same dates for everyone.`;return`Releasing on a shared schedule${launch===null?"":` since ${new Date(launch).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"})}`}, one more ${unit} ${every}. Join now and everything already out is yours.`}
|