@fayz-ai/inventory 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -2
  3. package/dist/chunk-2FXO63QV.js +1732 -0
  4. package/dist/chunk-2FXO63QV.js.map +1 -0
  5. package/dist/chunk-52AJ7H2W.js +116 -0
  6. package/dist/chunk-52AJ7H2W.js.map +1 -0
  7. package/dist/chunk-AFUR3HLA.js +84 -0
  8. package/dist/chunk-AFUR3HLA.js.map +1 -0
  9. package/dist/chunk-EF5LVTOC.js +65 -0
  10. package/dist/chunk-EF5LVTOC.js.map +1 -0
  11. package/dist/chunk-GBIFZCEN.js +33 -0
  12. package/dist/chunk-GBIFZCEN.js.map +1 -0
  13. package/dist/chunk-GDEIBHAB.js +14 -0
  14. package/dist/chunk-GDEIBHAB.js.map +1 -0
  15. package/dist/chunk-JN5AZ656.js +35 -0
  16. package/dist/chunk-JN5AZ656.js.map +1 -0
  17. package/dist/chunk-JX7NCXJQ.js +15 -0
  18. package/dist/chunk-JX7NCXJQ.js.map +1 -0
  19. package/dist/chunk-MCWG3PMP.js +549 -0
  20. package/dist/chunk-MCWG3PMP.js.map +1 -0
  21. package/dist/chunk-RUOX7OOY.js +30 -0
  22. package/dist/chunk-RUOX7OOY.js.map +1 -0
  23. package/dist/chunk-SZXUGORU.js +13 -0
  24. package/dist/chunk-SZXUGORU.js.map +1 -0
  25. package/dist/chunk-W36J5END.js +48 -0
  26. package/dist/chunk-W36J5END.js.map +1 -0
  27. package/dist/chunk-YZDGJBJD.js +53 -0
  28. package/dist/chunk-YZDGJBJD.js.map +1 -0
  29. package/dist/data/mock.d.ts +3 -0
  30. package/dist/data/mock.d.ts.map +1 -0
  31. package/dist/data/mock.js +10 -0
  32. package/dist/data/mock.js.map +1 -0
  33. package/dist/data/provider.d.ts +120 -0
  34. package/dist/data/provider.d.ts.map +1 -0
  35. package/dist/data/provider.js +3 -0
  36. package/dist/data/provider.js.map +1 -0
  37. package/dist/data/supabase.d.ts +3 -0
  38. package/dist/data/supabase.d.ts.map +1 -0
  39. package/dist/data/supabase.js +13 -0
  40. package/dist/data/supabase.js.map +1 -0
  41. package/dist/data/tables.d.ts +25 -0
  42. package/dist/data/tables.d.ts.map +1 -0
  43. package/dist/data/tables.js +3 -0
  44. package/dist/data/tables.js.map +1 -0
  45. package/dist/domain/consumption.d.ts +33 -0
  46. package/dist/domain/consumption.d.ts.map +1 -0
  47. package/dist/domain/consumption.js +3 -0
  48. package/dist/domain/consumption.js.map +1 -0
  49. package/dist/domain/count.d.ts +85 -0
  50. package/dist/domain/count.d.ts.map +1 -0
  51. package/dist/domain/count.js +3 -0
  52. package/dist/domain/count.js.map +1 -0
  53. package/dist/domain/movement.d.ts +101 -0
  54. package/dist/domain/movement.d.ts.map +1 -0
  55. package/dist/domain/movement.js +3 -0
  56. package/dist/domain/movement.js.map +1 -0
  57. package/dist/domain/product.d.ts +245 -0
  58. package/dist/domain/product.d.ts.map +1 -0
  59. package/dist/domain/product.js +3 -0
  60. package/dist/domain/product.js.map +1 -0
  61. package/dist/domain/query.d.ts +12 -0
  62. package/dist/domain/query.d.ts.map +1 -0
  63. package/dist/domain/query.js +3 -0
  64. package/dist/domain/query.js.map +1 -0
  65. package/dist/domain/recipe-sheet.d.ts +133 -0
  66. package/dist/domain/recipe-sheet.d.ts.map +1 -0
  67. package/dist/domain/recipe-sheet.js +3 -0
  68. package/dist/domain/recipe-sheet.js.map +1 -0
  69. package/dist/domain/recipe.d.ts +138 -0
  70. package/dist/domain/recipe.d.ts.map +1 -0
  71. package/dist/domain/recipe.js +3 -0
  72. package/dist/domain/recipe.js.map +1 -0
  73. package/dist/domain/reference-data.d.ts +37 -0
  74. package/dist/domain/reference-data.d.ts.map +1 -0
  75. package/dist/domain/reference-data.js +3 -0
  76. package/dist/domain/reference-data.js.map +1 -0
  77. package/dist/domain/stock.d.ts +132 -0
  78. package/dist/domain/stock.d.ts.map +1 -0
  79. package/dist/domain/stock.js +5 -0
  80. package/dist/domain/stock.js.map +1 -0
  81. package/dist/domain/usage-audit.d.ts +95 -0
  82. package/dist/domain/usage-audit.d.ts.map +1 -0
  83. package/dist/domain/usage-audit.js +3 -0
  84. package/dist/domain/usage-audit.js.map +1 -0
  85. package/dist/lib/idempotency.d.ts +4 -0
  86. package/dist/lib/idempotency.d.ts.map +1 -0
  87. package/dist/lib/idempotency.js +3 -0
  88. package/dist/lib/idempotency.js.map +1 -0
  89. package/dist/store.d.ts +52 -0
  90. package/dist/store.d.ts.map +1 -0
  91. package/dist/store.js +193 -0
  92. package/dist/store.js.map +1 -0
  93. package/package.json +50 -3
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domain/stock.ts"],"names":[],"mappings":";;;;AA+FO,SAAS,WAAA,CAAY,UAAkB,WAAA,EAAkC;AAC9E,EAAA,IAAI,QAAA,GAAW,GAAG,OAAO,UAAA;AACzB,EAAA,IAAI,QAAA,KAAa,GAAG,OAAO,MAAA;AAC3B,EAAA,IAAI,WAAA,GAAc,CAAA,IAAK,QAAA,GAAW,WAAA,EAAa,OAAO,WAAA;AACtD,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,WAAW,WAAA,EAAiD;AAC1E,EAAA,OAAO,MAAA,CAAO,WAAW,CAAA,GAAI,CAAA;AAC/B;AAEO,SAAS,aAAa,QAAA,EAA2B;AACtD,EAAA,OAAO,QAAA,IAAY,CAAA;AACrB;AAGO,SAAS,UAAA,CAAW,UAAkB,WAAA,EAA8B;AACzE,EAAA,OAAO,UAAA,CAAW,WAAW,CAAA,IAAK,QAAA,GAAW,KAAK,QAAA,IAAY,WAAA;AAChE;AAEO,SAAS,aAAA,CAAc,UAAkB,WAAA,EAAgD;AAC9F,EAAA,IAAI,YAAA,CAAa,QAAQ,CAAA,EAAG,OAAO,cAAA;AACnC,EAAA,IAAI,UAAA,CAAW,QAAA,EAAU,WAAW,CAAA,EAAG,OAAO,eAAA;AAC9C,EAAA,OAAO,MAAA;AACT;AAGO,SAAS,WAAA,CACd,MACA,KAAA,EACsC;AACtC,EAAA,MAAM,IAAA,GAAO,CAAC,MAAA,KAA2B,MAAA,KAAW,kBAAkB,CAAA,GAAI,CAAA;AAC1E,EAAA,OAAO,IAAA,CACJ,OAAA,CAAQ,CAAC,GAAA,KAAQ;AAChB,IAAA,MAAM,MAAA,GAAS,aAAA,CAAc,GAAA,CAAI,eAAA,EAAiB,IAAI,WAAW,CAAA;AACjE,IAAA,OAAO,MAAA,GAAS,CAAC,EAAE,GAAG,KAAK,MAAA,EAAQ,IAAI,EAAC;AAAA,EAC1C,CAAC,CAAA,CACA,IAAA,CAAK,CAAC,CAAA,EAAG,MAAM,IAAA,CAAK,CAAA,CAAE,MAAM,CAAA,GAAI,IAAA,CAAK,CAAA,CAAE,MAAM,CAAA,IAAK,CAAA,CAAE,IAAA,CAAK,aAAA,CAAc,CAAA,CAAE,IAAA,EAAM,OAAO,CAAC,CAAA,CACvF,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA;AACnB;AAGO,SAAS,qBAAqB,GAAA,EAAmB;AACtD,EAAA,MAAM,OAAA,GAAU,IAAI,IAAA,CAAK,GAAG,CAAA;AAC5B,EAAA,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,OAAA,EAAQ,GAAI,CAAC,CAAA;AACrC,EAAA,OAAO,OAAA,CAAQ,WAAA,EAAY,CAAE,KAAA,CAAM,GAAG,EAAE,CAAA;AAC1C;AASO,SAAS,cAAA,CACd,gBACA,mBAAA,EACkB;AAClB,EAAA,MAAM,kBAAkB,kBAAA,EAAmB;AAC3C,EAAA,KAAA,MAAW,QAAQ,mBAAA,EAAqB;AACtC,IAAA,IAAI,IAAA,IAAQ,eAAA,EAAiB,eAAA,CAAgB,IAAoB,CAAA,EAAA;AAAA,EACnE;AACA,EAAA,OAAO;AAAA,IACL,eAAe,cAAA,CAAe,MAAA;AAAA,IAC9B,aAAA,EAAe,cAAA,CAAe,MAAA,CAAO,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,CAAE,QAAA,EAAU,CAAA,CAAE,WAAW,CAAC,CAAA,CAAE,MAAA;AAAA,IACnF,eAAA,EAAiB,eAAe,MAAA,CAAO,CAAC,MAAM,YAAA,CAAa,CAAA,CAAE,QAAQ,CAAC,CAAA,CAAE,MAAA;AAAA,IACxE,eAAA,EAAiB,cAAA,CAAe,MAAA,CAAO,CAAC,GAAA,EAAK,CAAA,KAAM,GAAA,GAAA,CAAO,CAAA,CAAE,QAAA,IAAY,CAAA,KAAM,CAAA,CAAE,SAAA,IAAa,IAAI,CAAC,CAAA;AAAA,IAClG,qBAAqB,mBAAA,CAAoB,MAAA;AAAA,IACzC,eAAA;AAAA,IACA,cAAA,EAAgB,cAAA,CAAe,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,UAAA,CAAW,CAAA,CAAE,WAAW,CAAC,CAAA,CAAE;AAAA,GAC3E;AACF;AAGO,SAAS,oBAAoB,OAAA,EAAiG;AACnI,EAAA,MAAM,KAAA,GAAQ,SAAS,aAAA,IAAiB,CAAA;AACxC,EAAA,OAAO,KAAA,GAAQ,CAAA,IAAA,CAAM,OAAA,EAAS,cAAA,IAAkB,CAAA,MAAO,KAAA;AACzD;AAGO,SAAS,cAAA,CACd,aACA,MAAA,EACiB;AACjB,EAAA,MAAM,KAAA,uBAAY,GAAA,EAA2B;AAC7C,EAAA,KAAA,MAAW,EAAA,IAAM,WAAA,EAAa,KAAA,CAAM,GAAA,CAAI,EAAA,IAAM,IAAA,EAAA,CAAO,KAAA,CAAM,GAAA,CAAI,EAAA,IAAM,IAAI,CAAA,IAAK,CAAA,IAAK,CAAC,CAAA;AACpF,EAAA,OAAO,CAAC,GAAG,KAAA,CAAM,OAAA,EAAS,CAAA,CACvB,GAAA,CAAI,CAAC,CAAC,UAAA,EAAY,KAAK,CAAA,MAAO;AAAA,IAC7B,UAAA;AAAA,IACA,IAAA,EAAM,UAAA,GAAc,MAAA,CAAO,UAAU,KAAK,oBAAA,GAAwB,eAAA;AAAA,IAClE;AAAA,GACF,CAAE,EACD,IAAA,CAAK,CAAC,GAAG,CAAA,KAAM,CAAA,CAAE,KAAA,GAAQ,CAAA,CAAE,KAAK,CAAA;AACrC;AAQO,SAAS,YAAA,CAAa,QAAkC,KAAA,EAAuC;AACpG,EAAA,MAAM,GAAA,GAAM,CAAC,CAAA,KAAc,UAAA,CAAW,OAAO,CAAC,CAAA;AAC9C,EAAA,QAAQ,MAAA;AAAQ,IACd,KAAK,SAAA;AAAW,MAAA,OAAO,EAAE,IAAA,EAAM,SAAA,EAAW,MAAA,EAAQ,GAAA,CAAI,CAAC,CAAA,EAAE;AAAA,IACzD,KAAK,MAAA;AAAQ,MAAA,OAAO,EAAE,MAAM,MAAA,EAAO;AAAA,IACnC,KAAK,MAAA;AAAQ,MAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,GAAA,CAAI,CAAC,CAAA,EAAG,EAAA,EAAI,GAAA,CAAI,EAAE,CAAA,EAAE;AAAA,IAChE,KAAK,MAAA;AAAQ,MAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,GAAA,CAAI,CAAC,CAAA,EAAG,EAAA,EAAI,GAAA,CAAI,EAAE,CAAA,EAAE;AAAA,IAChE,KAAK,MAAA;AAAQ,MAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,GAAA,CAAI,CAAC,CAAA,EAAG,EAAA,EAAI,GAAA,CAAI,EAAE,CAAA,EAAE;AAAA,IAChE;AAAS,MAAA,OAAO,MAAA;AAAA;AAEpB;AAEO,SAAS,gBAAA,CAAiB,QAAkC,cAAA,EAA6C;AAC9G,EAAA,IAAI,CAAC,QAAQ,OAAO,IAAA;AACpB,EAAA,IAAI,MAAA,CAAO,IAAA,KAAS,MAAA,EAAQ,OAAO,CAAC,cAAA;AACpC,EAAA,IAAI,CAAC,gBAAgB,OAAO,KAAA;AAC5B,EAAA,IAAI,MAAA,CAAO,IAAA,KAAS,SAAA,EAAW,OAAO,iBAAiB,MAAA,CAAO,MAAA;AAC9D,EAAA,OAAO,cAAA,IAAkB,MAAA,CAAO,IAAA,IAAQ,cAAA,IAAkB,MAAA,CAAO,EAAA;AACnE;AAGO,SAAS,UAAA,CACd,QAAA,EACA,OAAA,EACA,iBAAA,EACe;AACf,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,QAAA,CAAS,QAAQ,CAAA,IAAK,CAAA;AAC9C,EAAA,MAAM,WAAA,GAAc,SAAS,WAAA,IAAe,CAAA;AAC5C,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,QAAA,CAAS,QAAQ,CAAA,IAAK,CAAA;AAC9C,EAAA,OAAO;AAAA,IACL,IAAI,QAAA,CAAS,EAAA;AAAA,IACb,WAAW,QAAA,CAAS,SAAA;AAAA,IACpB,WAAA,EAAa,SAAS,IAAA,IAAQ,QAAA;AAAA,IAC9B,YAAY,OAAA,EAAS,UAAA;AAAA,IACrB,cAAc,OAAA,EAAS,YAAA;AAAA,IACvB,iBAAiB,QAAA,CAAS,eAAA;AAAA,IAC1B,iBAAA;AAAA,IACA,aAAa,QAAA,CAAS,WAAA;AAAA,IACtB,gBAAgB,QAAA,CAAS,cAAA;AAAA,IACzB,QAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA;AAAA,IACA,YAAY,QAAA,GAAW,QAAA;AAAA,IACvB,MAAA,EAAQ,WAAA,CAAY,QAAA,EAAU,WAAW;AAAA,GAC3C;AACF;AAEO,SAAS,SAAA,CAAU,KAAoC,QAAA,EAA8C;AAC1G,EAAA,OAAO,CAAC,YAAY,QAAA,CAAS,MAAA,KAAW,KAAK,QAAA,CAAS,QAAA,CAAS,IAAI,MAAM,CAAA;AAC3E","file":"chunk-52AJ7H2W.js","sourcesContent":["import { emptyMovementTally, type MovementType } from './movement'\nimport { isoDayFrom } from './query'\n\nexport interface StockLocation {\n id: string\n name: string\n description?: string\n isActive: boolean\n unitId?: string\n tenantId: string\n createdAt: string\n updatedAt: string\n}\n\n/** One slot of stock: product × location × batch × expiry. */\nexport interface StockPosition {\n id: string\n productId: string\n productName?: string\n quantity: number\n unitCost: number\n stockLocationId?: string\n stockLocationName?: string\n batchNumber?: string\n expirationDate?: string\n measurementUnitId?: string\n unitType?: 'base' | 'purchase' | 'content'\n tenantId: string\n createdAt: string\n updatedAt?: string\n}\n\n/** `negative` exists because the ledger allows an exit posted before the entry that feeds it. */\nexport type StockHealth = 'negative' | 'zero' | 'below_min' | 'ok'\n\nexport type ExpiryFilter = 'all' | 'expired' | 'le30' | 'le60' | 'le90' | 'none'\n\nexport interface StockPanelQuery {\n stockLocationId?: string\n categoryId?: string\n productSearch?: string\n batch?: string\n expiry?: ExpiryFilter\n /** Empty means every health. */\n status?: StockHealth[]\n limit?: number\n}\n\nexport interface StockPanelRow {\n id: string\n productId: string\n productName: string\n categoryId?: string\n categoryName?: string\n stockLocationId?: string\n stockLocationName?: string\n batchNumber?: string\n expirationDate?: string\n quantity: number\n minQuantity: number\n unitCost: number\n totalValue: number\n health: StockHealth\n}\n\nexport interface InventorySummary {\n totalProducts: number\n lowStockCount: number\n outOfStockCount: number\n totalStockValue: number\n recentMovementCount: number\n movementsByType: Record<MovementType, number>\n /** Active products with no minimum set — why \"low stock\" can read zero with an empty shelf. */\n noMinimumCount: number\n}\n\nexport type RestockReason = 'out-of-stock' | 'below-minimum'\n\nexport interface RestockLine {\n productId: string\n name: string\n categoryName?: string\n currentQuantity: number\n minQuantity: number\n unit?: string\n reason: RestockReason\n}\n\nexport interface CategoryCount {\n categoryId: string | null\n name: string\n count: number\n}\n\n/** A minimum of zero means \"none set\", so a product without one never reads as below it. */\nexport function stockHealth(quantity: number, minQuantity: number): StockHealth {\n if (quantity < 0) return 'negative'\n if (quantity === 0) return 'zero'\n if (minQuantity > 0 && quantity < minQuantity) return 'below_min'\n return 'ok'\n}\n\nexport function hasMinimum(minQuantity: number | null | undefined): boolean {\n return Number(minQuantity) > 0\n}\n\nexport function isOutOfStock(quantity: number): boolean {\n return quantity <= 0\n}\n\n/** The alarm rule the summary, the list and the restock queue share. */\nexport function isLowStock(quantity: number, minQuantity: number): boolean {\n return hasMinimum(minQuantity) && quantity > 0 && quantity <= minQuantity\n}\n\nexport function restockReason(quantity: number, minQuantity: number): RestockReason | undefined {\n if (isOutOfStock(quantity)) return 'out-of-stock'\n if (isLowStock(quantity, minQuantity)) return 'below-minimum'\n return undefined\n}\n\n/** What needs buying: below-minimum first (a decided purchase), then zeroed, each by name. */\nexport function pickRestock<T extends { name: string; currentQuantity: number; minQuantity: number }>(\n rows: T[],\n limit: number,\n): Array<T & { reason: RestockReason }> {\n const rank = (reason: RestockReason) => (reason === 'below-minimum' ? 0 : 1)\n return rows\n .flatMap((row) => {\n const reason = restockReason(row.currentQuantity, row.minQuantity)\n return reason ? [{ ...row, reason }] : []\n })\n .sort((a, b) => rank(a.reason) - rank(b.reason) || a.name.localeCompare(b.name, 'pt-BR'))\n .slice(0, limit)\n}\n\n/** The day a movement must be on or after to count as recent: seven days back, as a UTC date. */\nexport function recentMovementCutoff(now: Date): string {\n const weekAgo = new Date(now)\n weekAgo.setDate(weekAgo.getDate() - 7)\n return weekAgo.toISOString().slice(0, 10)\n}\n\nexport interface SummarizableProduct {\n quantity: number\n minQuantity: number\n /** The price the adapter values stock at. */\n unitValue: number\n}\n\nexport function summarizeStock(\n activeProducts: SummarizableProduct[],\n recentMovementTypes: string[],\n): InventorySummary {\n const movementsByType = emptyMovementTally()\n for (const type of recentMovementTypes) {\n if (type in movementsByType) movementsByType[type as MovementType]++\n }\n return {\n totalProducts: activeProducts.length,\n lowStockCount: activeProducts.filter((p) => isLowStock(p.quantity, p.minQuantity)).length,\n outOfStockCount: activeProducts.filter((p) => isOutOfStock(p.quantity)).length,\n totalStockValue: activeProducts.reduce((sum, p) => sum + (p.quantity || 0) * (p.unitValue || 0), 0),\n recentMovementCount: recentMovementTypes.length,\n movementsByType,\n noMinimumCount: activeProducts.filter((p) => !hasMinimum(p.minQuantity)).length,\n }\n}\n\n/** Nobody filled in a minimum, so the low-stock alarm cannot fire yet. */\nexport function noProductHasMinimum(summary: Pick<InventorySummary, 'totalProducts' | 'noMinimumCount'> | null | undefined): boolean {\n const total = summary?.totalProducts ?? 0\n return total > 0 && (summary?.noMinimumCount ?? 0) === total\n}\n\n/** Products per category, biggest first; the uncategorised bucket is named because it is an answer, not a hole. */\nexport function categoryCounts(\n categoryIds: Array<string | null | undefined>,\n nameOf: (id: string) => string | undefined,\n): CategoryCount[] {\n const tally = new Map<string | null, number>()\n for (const id of categoryIds) tally.set(id ?? null, (tally.get(id ?? null) ?? 0) + 1)\n return [...tally.entries()]\n .map(([categoryId, count]) => ({\n categoryId,\n name: categoryId ? (nameOf(categoryId) ?? 'Categoria removida') : 'Sem categoria',\n count,\n }))\n .sort((a, b) => b.count - a.count)\n}\n\n/** An expiry filter as a date window relative to `today`; undefined when it filters nothing. */\nexport type ExpiryWindow =\n | { kind: 'expired'; before: string }\n | { kind: 'none' }\n | { kind: 'within'; from: string; to: string }\n\nexport function expiryWindow(expiry: ExpiryFilter | undefined, today: Date): ExpiryWindow | undefined {\n const day = (d: number) => isoDayFrom(today, d)\n switch (expiry) {\n case 'expired': return { kind: 'expired', before: day(0) }\n case 'none': return { kind: 'none' }\n case 'le30': return { kind: 'within', from: day(0), to: day(30) }\n case 'le60': return { kind: 'within', from: day(0), to: day(60) }\n case 'le90': return { kind: 'within', from: day(0), to: day(90) }\n default: return undefined\n }\n}\n\nexport function isInExpiryWindow(window: ExpiryWindow | undefined, expirationDate: string | undefined): boolean {\n if (!window) return true\n if (window.kind === 'none') return !expirationDate\n if (!expirationDate) return false\n if (window.kind === 'expired') return expirationDate < window.before\n return expirationDate >= window.from && expirationDate <= window.to\n}\n\n/** A panel line for one slot; a slot whose product is not in hand still shows, because hiding it would hide stock. */\nexport function toPanelRow(\n position: Pick<StockPosition, 'id' | 'productId' | 'quantity' | 'unitCost' | 'stockLocationId' | 'batchNumber' | 'expirationDate'>,\n product: { name: string; minQuantity: number; categoryId?: string; categoryName?: string } | undefined,\n stockLocationName: string | undefined,\n): StockPanelRow {\n const quantity = Number(position.quantity) || 0\n const minQuantity = product?.minQuantity ?? 0\n const unitCost = Number(position.unitCost) || 0\n return {\n id: position.id,\n productId: position.productId,\n productName: product?.name ?? '—',\n categoryId: product?.categoryId,\n categoryName: product?.categoryName,\n stockLocationId: position.stockLocationId,\n stockLocationName,\n batchNumber: position.batchNumber,\n expirationDate: position.expirationDate,\n quantity,\n minQuantity,\n unitCost,\n totalValue: quantity * unitCost,\n health: stockHealth(quantity, minQuantity),\n }\n}\n\nexport function hasHealth(row: Pick<StockPanelRow, 'health'>, statuses: StockHealth[] | undefined): boolean {\n return !statuses || statuses.length === 0 || statuses.includes(row.health)\n}\n"]}
@@ -0,0 +1,84 @@
1
+ // src/domain/movement.ts
2
+ var MOVEMENT_TYPES = [
3
+ { value: "entry", labelKey: "inventory.movementType.entry", icon: "ArrowDownToLine", effect: "increase" },
4
+ { value: "exit", labelKey: "inventory.movementType.exit", icon: "ArrowUpFromLine", effect: "decrease" },
5
+ { value: "adjustment", labelKey: "inventory.movementType.adjustment", icon: "Scale", effect: "set" },
6
+ { value: "transfer", labelKey: "inventory.movementType.transfer", icon: "ArrowLeftRight", effect: "move" },
7
+ { value: "loss", labelKey: "inventory.movementType.loss", icon: "TriangleAlert", effect: "decrease" }
8
+ ];
9
+ var MOVEMENT_TYPE_VALUES = MOVEMENT_TYPES.map((m) => m.value);
10
+ var LEGACY_PT_MOVEMENT_TYPE = {
11
+ entrada: "entry",
12
+ saida: "exit",
13
+ "sa\xEDda": "exit",
14
+ ajuste: "adjustment",
15
+ transferencia: "transfer",
16
+ "transfer\xEAncia": "transfer",
17
+ perda: "loss"
18
+ };
19
+ var LEDGER_KIND_TO_TYPE = {
20
+ in: "entry",
21
+ out: "exit",
22
+ transfer: "transfer",
23
+ adjust: "adjustment",
24
+ reverse: "adjustment"
25
+ };
26
+ var TYPE_TO_LEDGER_KIND = {
27
+ entry: "in",
28
+ exit: "out",
29
+ transfer: "transfer",
30
+ adjustment: "adjust",
31
+ // A loss is an adjustment whose reason says what happened.
32
+ loss: "adjust"
33
+ };
34
+ function normalizeMovementType(raw) {
35
+ const v = raw.trim().toLowerCase();
36
+ if (MOVEMENT_TYPE_VALUES.includes(v)) return v;
37
+ return LEDGER_KIND_TO_TYPE[v] ?? LEGACY_PT_MOVEMENT_TYPE[v];
38
+ }
39
+ function movementTypeSpec(value) {
40
+ const type = normalizeMovementType(value);
41
+ return type ? MOVEMENT_TYPES.find((m) => m.value === type) : void 0;
42
+ }
43
+ function ledgerKind(type) {
44
+ return TYPE_TO_LEDGER_KIND[type] ?? "adjust";
45
+ }
46
+ function signedLedgerQuantity(kind, quantity) {
47
+ if (kind === "out" || kind === "transfer") return -Math.abs(quantity);
48
+ if (kind === "in") return Math.abs(quantity);
49
+ return quantity;
50
+ }
51
+ function sourcePositionChange(type, quantity) {
52
+ if (type === "adjustment") return { mode: "set", amount: quantity };
53
+ return { mode: "add", amount: type === "entry" ? quantity : -quantity };
54
+ }
55
+ function reversedSourceDelta(type, quantity) {
56
+ return type === "entry" ? -quantity : quantity;
57
+ }
58
+ function onHandDelta(type, quantity) {
59
+ if (type === "entry") return quantity;
60
+ if (type === "exit" || type === "loss") return -quantity;
61
+ return 0;
62
+ }
63
+ function movementNeedsReason(type) {
64
+ return type === "adjustment" || type === "loss";
65
+ }
66
+ function movementNeedsDestination(type) {
67
+ return type === "transfer";
68
+ }
69
+ function movementTakesSupplier(type) {
70
+ return type === "entry";
71
+ }
72
+ function canRecordMovement(input) {
73
+ if (!(input.quantity > 0)) return false;
74
+ if (movementNeedsReason(input.movementType) && !input.reason?.trim()) return false;
75
+ if (movementNeedsDestination(input.movementType) && !input.destinationLocationId) return false;
76
+ return true;
77
+ }
78
+ function emptyMovementTally() {
79
+ return { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 };
80
+ }
81
+
82
+ export { LEGACY_PT_MOVEMENT_TYPE, MOVEMENT_TYPES, MOVEMENT_TYPE_VALUES, canRecordMovement, emptyMovementTally, ledgerKind, movementNeedsDestination, movementNeedsReason, movementTakesSupplier, movementTypeSpec, normalizeMovementType, onHandDelta, reversedSourceDelta, signedLedgerQuantity, sourcePositionChange };
83
+ //# sourceMappingURL=chunk-AFUR3HLA.js.map
84
+ //# sourceMappingURL=chunk-AFUR3HLA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domain/movement.ts"],"names":[],"mappings":"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","file":"chunk-AFUR3HLA.js","sourcesContent":["import type { DateRange } from './query'\n\nexport type MovementType = 'entry' | 'exit' | 'adjustment' | 'transfer' | 'loss'\n\n/** The ledger's own words, fixed by a CHECK constraint on `plg_inventory_stock_movements.kind`. */\nexport type LedgerKind = 'in' | 'out' | 'transfer' | 'adjust' | 'reverse'\n\nexport interface StockMovement {\n id: string\n productId: string\n productName?: string\n quantity: number\n movementType: MovementType\n unitCost: number\n totalCost: number\n stockLocationId?: string\n stockLocationName?: string\n destinationLocationId?: string\n destinationLocationName?: string\n supplierId?: string\n supplierName?: string\n documentNumber?: string\n /** `nfe`, `manual`, `transfer`… — what tells whether the source document can be opened. */\n documentType?: string\n /** The stock operation that wrote this movement; the way back to the document that caused it. */\n operationId?: string\n measurementUnitId?: string\n measurementUnitAbbreviation?: string\n reason?: string\n notes?: string\n batchNumber?: string\n expirationDate?: string\n movementDate: string\n userId?: string\n userName?: string\n metadata?: Record<string, unknown>\n tenantId: string\n createdAt: string\n}\n\nexport interface CreateStockMovementInput {\n productId: string\n quantity: number\n movementType: MovementType\n unitCost?: number\n stockLocationId?: string\n destinationLocationId?: string\n supplierId?: string\n documentNumber?: string\n reason?: string\n notes?: string\n movementDate?: string\n batchNumber?: string\n expirationDate?: string\n /** Stable for every retry of the same operator action. */\n idempotencyKey?: string\n}\n\nexport interface MovementQuery {\n productId?: string\n movementType?: MovementType | MovementType[]\n stockLocationId?: string\n dateRange?: DateRange\n search?: string\n page?: number\n pageSize?: number\n}\n\nexport interface MovementTypeSpec {\n value: MovementType\n /** i18n key under the plugin's `inventory.movementType` namespace. */\n labelKey: string\n /** lucide icon name, resolved by the host's icon map. */\n icon: string\n /** What the quantity does to the on-hand total. */\n effect: 'increase' | 'decrease' | 'set' | 'move'\n}\n\nexport const MOVEMENT_TYPES: readonly MovementTypeSpec[] = [\n { value: 'entry', labelKey: 'inventory.movementType.entry', icon: 'ArrowDownToLine', effect: 'increase' },\n { value: 'exit', labelKey: 'inventory.movementType.exit', icon: 'ArrowUpFromLine', effect: 'decrease' },\n { value: 'adjustment', labelKey: 'inventory.movementType.adjustment', icon: 'Scale', effect: 'set' },\n { value: 'transfer', labelKey: 'inventory.movementType.transfer', icon: 'ArrowLeftRight', effect: 'move' },\n { value: 'loss', labelKey: 'inventory.movementType.loss', icon: 'TriangleAlert', effect: 'decrease' },\n] as const\n\nexport const MOVEMENT_TYPE_VALUES: readonly MovementType[] = MOVEMENT_TYPES.map((m) => m.value)\n\n/** Portuguese spellings pre-SDK apps imported; kept beside the canonical list so an import cannot drift from what the UI renders. */\nexport const LEGACY_PT_MOVEMENT_TYPE: Readonly<Record<string, MovementType>> = {\n entrada: 'entry',\n saida: 'exit',\n 'saída': 'exit',\n ajuste: 'adjustment',\n transferencia: 'transfer',\n 'transferência': 'transfer',\n perda: 'loss',\n} as const\n\nconst LEDGER_KIND_TO_TYPE: Readonly<Record<LedgerKind, MovementType>> = {\n in: 'entry',\n out: 'exit',\n transfer: 'transfer',\n adjust: 'adjustment',\n reverse: 'adjustment',\n}\n\nconst TYPE_TO_LEDGER_KIND: Readonly<Record<MovementType, LedgerKind>> = {\n entry: 'in',\n exit: 'out',\n transfer: 'transfer',\n adjustment: 'adjust',\n // A loss is an adjustment whose reason says what happened.\n loss: 'adjust',\n}\n\n/** Any known spelling (canonical, ledger or legacy Portuguese) to a MovementType; undefined when unrecognised. */\nexport function normalizeMovementType(raw: string): MovementType | undefined {\n const v = raw.trim().toLowerCase()\n if ((MOVEMENT_TYPE_VALUES as readonly string[]).includes(v)) return v as MovementType\n return LEDGER_KIND_TO_TYPE[v as LedgerKind] ?? LEGACY_PT_MOVEMENT_TYPE[v]\n}\n\nexport function movementTypeSpec(value: string): MovementTypeSpec | undefined {\n const type = normalizeMovementType(value)\n return type ? MOVEMENT_TYPES.find((m) => m.value === type) : undefined\n}\n\nexport function ledgerKind(type: MovementType | string): LedgerKind {\n return TYPE_TO_LEDGER_KIND[type as MovementType] ?? 'adjust'\n}\n\n/** The ledger's sign check: an `out` or a `transfer` leaves negative, an `in` arrives positive. */\nexport function signedLedgerQuantity(kind: LedgerKind, quantity: number): number {\n if (kind === 'out' || kind === 'transfer') return -Math.abs(quantity)\n if (kind === 'in') return Math.abs(quantity)\n return quantity\n}\n\n/** How a movement changes its source slot: an adjustment is the counted truth, everything else a delta. */\nexport function sourcePositionChange(type: MovementType, quantity: number): { mode: 'add' | 'set'; amount: number } {\n if (type === 'adjustment') return { mode: 'set', amount: quantity }\n return { mode: 'add', amount: type === 'entry' ? quantity : -quantity }\n}\n\n/** What undoing a movement does to its source slot; the destination of a transfer gives back `-quantity`. */\nexport function reversedSourceDelta(type: MovementType, quantity: number): number {\n return type === 'entry' ? -quantity : quantity\n}\n\n/** What a movement does to the product's on-hand total when no slot decides it. */\nexport function onHandDelta(type: MovementType, quantity: number): number {\n if (type === 'entry') return quantity\n if (type === 'exit' || type === 'loss') return -quantity\n return 0\n}\n\nexport function movementNeedsReason(type: MovementType): boolean {\n return type === 'adjustment' || type === 'loss'\n}\n\nexport function movementNeedsDestination(type: MovementType): boolean {\n return type === 'transfer'\n}\n\nexport function movementTakesSupplier(type: MovementType): boolean {\n return type === 'entry'\n}\n\nexport function canRecordMovement(input: {\n movementType: MovementType\n quantity: number\n reason?: string\n destinationLocationId?: string\n}): boolean {\n if (!(input.quantity > 0)) return false\n if (movementNeedsReason(input.movementType) && !input.reason?.trim()) return false\n if (movementNeedsDestination(input.movementType) && !input.destinationLocationId) return false\n return true\n}\n\nexport function emptyMovementTally(): Record<MovementType, number> {\n return { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 }\n}\n"]}
@@ -0,0 +1,65 @@
1
+ // src/domain/recipe.ts
2
+ var CMV_ALERT_THRESHOLD = 65;
3
+ var CMV_GOOD_THRESHOLD = 33;
4
+ function ingredientUnitCost(basis) {
5
+ if (!basis) return 0;
6
+ const cost = Number(basis.cost) || 0;
7
+ if (!basis.purchaseUnitId) return cost;
8
+ const factor = Number(basis.conversionFactor) || 0;
9
+ return factor > 0 ? cost / factor : cost;
10
+ }
11
+ function lineCost(line) {
12
+ return ingredientUnitCost(line.costBasis) * (Number(line.quantity) || 0);
13
+ }
14
+ function costLines(lines) {
15
+ return lines.map((line) => {
16
+ const unitCost = ingredientUnitCost(line.costBasis);
17
+ return { line, unitCost, cost: unitCost * (Number(line.quantity) || 0) };
18
+ });
19
+ }
20
+ function recipeCost(lines) {
21
+ return lines.reduce((sum, line) => sum + lineCost(line), 0);
22
+ }
23
+ function cmvPercent(cost, salePrice) {
24
+ const price = Number(salePrice) || 0;
25
+ if (price <= 0) return null;
26
+ return (Number(cost) || 0) / price * 100;
27
+ }
28
+ function isCmvHigh(cmv) {
29
+ return typeof cmv === "number" && cmv > CMV_ALERT_THRESHOLD;
30
+ }
31
+ function cmvBand(cmv) {
32
+ if (typeof cmv !== "number") return null;
33
+ return cmv > CMV_ALERT_THRESHOLD ? "high" : cmv < CMV_GOOD_THRESHOLD ? "good" : "attention";
34
+ }
35
+ var CMV_TONE = { good: "text-success", attention: "", high: "text-destructive" };
36
+ function activeRecipeFor(recipes, productId) {
37
+ return recipes.filter((r) => r.productId === productId && r.isActive).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
38
+ }
39
+ var round4 = (n) => Math.round(n * 1e4) / 1e4;
40
+ function explodeRecipe(recipe, ingredients, soldQuantity) {
41
+ if (!Number.isFinite(recipe.yieldQuantity) || recipe.yieldQuantity <= 0) {
42
+ throw new Error(`Recipe ${recipe.id} has an invalid yield quantity`);
43
+ }
44
+ if (!ingredients.length) throw new Error(`Recipe ${recipe.id} has no ingredients`);
45
+ const factor = soldQuantity / recipe.yieldQuantity;
46
+ const lines = /* @__PURE__ */ new Map();
47
+ for (const ingredient of ingredients) {
48
+ if (!ingredient.locationId) {
49
+ throw new Error(`Ingredient ${ingredient.productId} has no default stock location`);
50
+ }
51
+ const quantity = round4(Number(ingredient.quantity) * factor);
52
+ if (!Number.isFinite(quantity) || quantity <= 0) {
53
+ throw new Error(`Recipe ${recipe.id} produced an invalid quantity for ingredient ${ingredient.productId}`);
54
+ }
55
+ const key = `${ingredient.productId}:${ingredient.locationId}:${ingredient.unitId ?? ""}`;
56
+ const current = lines.get(key);
57
+ if (current) current.quantity = round4(current.quantity + quantity);
58
+ else lines.set(key, { productId: ingredient.productId, locationId: ingredient.locationId, quantity, unitId: ingredient.unitId });
59
+ }
60
+ return [...lines.values()];
61
+ }
62
+
63
+ export { CMV_ALERT_THRESHOLD, CMV_GOOD_THRESHOLD, CMV_TONE, activeRecipeFor, cmvBand, cmvPercent, costLines, explodeRecipe, ingredientUnitCost, isCmvHigh, lineCost, recipeCost };
64
+ //# sourceMappingURL=chunk-EF5LVTOC.js.map
65
+ //# sourceMappingURL=chunk-EF5LVTOC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domain/recipe.ts"],"names":[],"mappings":"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","file":"chunk-EF5LVTOC.js","sourcesContent":["export interface Recipe {\n id: string\n name: string\n description?: string\n productId: string\n productName?: string\n /** Sale price of the produced product — the denominator of CMV%. */\n productSalePrice?: number\n yieldQuantity: number\n yieldUnitId?: string\n yieldUnitName?: string\n /** portion = the sheet is one unit sold; batch = the sheet produces the yield. */\n yieldMode?: 'portion' | 'batch'\n preparationTimeMinutes?: number\n instructions?: string\n isActive: boolean\n ingredientCount?: number\n /** Carries what the assistant could not resolve — see RecipeGaps. */\n metadata?: Record<string, unknown>\n tenantId: string\n createdAt: string\n updatedAt: string\n}\n\n/** What an ingredient product contributes to a recipe line's cost. */\nexport interface IngredientCostBasis {\n cost: number\n /** Base units per purchase unit. */\n conversionFactor?: number\n /** Set only when the product is bought in a unit other than the counted one. */\n purchaseUnitId?: string\n}\n\n/** What the assistant proposed but could not match to the catalogue. */\nexport interface RecipeGaps {\n unmatchedIngredients?: Array<{ name?: string; quantity?: number; notes?: string }>\n productHint?: string\n}\n\nexport interface RecipeIngredient {\n id: string\n recipeId: string\n productId: string\n productName?: string\n quantity: number\n unitId?: string\n unitName?: string\n displayOrder: number\n notes?: string\n /** Reading unit of the line; quantity itself stays in the base unit. */\n displayUnitId?: string\n groupId?: string\n groupName?: string\n costBasis?: IngredientCostBasis\n createdAt: string\n}\n\n/** A stage of a recipe — \"massa\", \"recheio\", \"montagem\". */\nexport interface RecipeGroup {\n id: string\n recipeId: string\n name: string\n preparationNotes?: string\n displayOrder: number\n estimatedMinutes?: number\n stationId?: string\n tenantId: string\n createdAt: string\n updatedAt: string\n}\n\nexport interface CreateRecipeIngredientInput {\n productId: string\n quantity: number\n unitId?: string\n displayOrder?: number\n notes?: string\n groupId?: string\n}\n\nexport interface CreateRecipeInput {\n name: string\n description?: string\n productId: string\n yieldQuantity: number\n yieldUnitId?: string\n preparationTimeMinutes?: number\n instructions?: string\n ingredients: CreateRecipeIngredientInput[]\n}\n\n/** Above this the dish eats the margin (the legacy app drew it at 35; the owner set 65). */\nexport const CMV_ALERT_THRESHOLD = 65\n/** Below this the dish is healthy — food-service practice puts a good CMV at 25–33%. */\nexport const CMV_GOOD_THRESHOLD = 33\n\nexport interface CostableLine {\n quantity: number\n costBasis?: IngredientCostBasis | null\n}\n\nexport interface CostedLine<T> {\n line: T\n /** Cost of one unit as the recipe counts it. */\n unitCost: number\n cost: number\n}\n\n/**\n * A recipe line's quantity is assumed to be in the ingredient's own unit. Only a product bought in another\n * unit converts: its cost is per purchase unit, so the per-unit cost is cost ÷ factor.\n */\nexport function ingredientUnitCost(basis?: IngredientCostBasis | null): number {\n if (!basis) return 0\n const cost = Number(basis.cost) || 0\n if (!basis.purchaseUnitId) return cost\n const factor = Number(basis.conversionFactor) || 0\n return factor > 0 ? cost / factor : cost\n}\n\nexport function lineCost(line: CostableLine): number {\n return ingredientUnitCost(line.costBasis) * (Number(line.quantity) || 0)\n}\n\nexport function costLines<T extends CostableLine>(lines: T[]): CostedLine<T>[] {\n return lines.map((line) => {\n const unitCost = ingredientUnitCost(line.costBasis)\n return { line, unitCost, cost: unitCost * (Number(line.quantity) || 0) }\n })\n}\n\nexport function recipeCost(lines: CostableLine[]): number {\n return lines.reduce((sum, line) => sum + lineCost(line), 0)\n}\n\n/** Cost as a percentage of the sale price. Null when there is no price to divide by. */\nexport function cmvPercent(cost: number, salePrice?: number | null): number | null {\n const price = Number(salePrice) || 0\n if (price <= 0) return null\n return ((Number(cost) || 0) / price) * 100\n}\n\nexport function isCmvHigh(cmv: number | null | undefined): boolean {\n return typeof cmv === 'number' && cmv > CMV_ALERT_THRESHOLD\n}\n\nexport type CmvBand = 'good' | 'attention' | 'high'\n\nexport function cmvBand(cmv: number | null | undefined): CmvBand | null {\n if (typeof cmv !== 'number') return null\n return cmv > CMV_ALERT_THRESHOLD ? 'high' : cmv < CMV_GOOD_THRESHOLD ? 'good' : 'attention'\n}\n\n/** Text colour for a CMV figure; the middle band stays neutral. */\nexport const CMV_TONE: Record<CmvBand, string> = { good: 'text-success', attention: '', high: 'text-destructive' }\n\n/** The newest active recipe of a product decides consumption; there should be only one. */\nexport function activeRecipeFor<T extends Pick<Recipe, 'productId' | 'isActive' | 'updatedAt'>>(\n recipes: T[],\n productId: string,\n): T | undefined {\n return recipes\n .filter((r) => r.productId === productId && r.isActive)\n .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0]\n}\n\nexport interface ExplodableIngredient {\n productId: string\n quantity: number\n /** The slot the ingredient leaves from: its default stock location. */\n locationId?: string\n unitId?: string\n}\n\nexport interface ConsumptionLine {\n productId: string\n locationId: string\n quantity: number\n unitId?: string\n}\n\nconst round4 = (n: number) => Math.round(n * 10_000) / 10_000\n\n/**\n * What selling `soldQuantity` of a recipe's product takes out of stock: each ingredient scaled by\n * sold ÷ yield, rounded to 4 places, and merged per (product, location, unit).\n */\nexport function explodeRecipe(\n recipe: Pick<Recipe, 'id' | 'yieldQuantity'>,\n ingredients: ExplodableIngredient[],\n soldQuantity: number,\n): ConsumptionLine[] {\n if (!Number.isFinite(recipe.yieldQuantity) || recipe.yieldQuantity <= 0) {\n throw new Error(`Recipe ${recipe.id} has an invalid yield quantity`)\n }\n if (!ingredients.length) throw new Error(`Recipe ${recipe.id} has no ingredients`)\n\n const factor = soldQuantity / recipe.yieldQuantity\n const lines = new Map<string, ConsumptionLine>()\n for (const ingredient of ingredients) {\n if (!ingredient.locationId) {\n throw new Error(`Ingredient ${ingredient.productId} has no default stock location`)\n }\n const quantity = round4(Number(ingredient.quantity) * factor)\n if (!Number.isFinite(quantity) || quantity <= 0) {\n throw new Error(`Recipe ${recipe.id} produced an invalid quantity for ingredient ${ingredient.productId}`)\n }\n const key = `${ingredient.productId}:${ingredient.locationId}:${ingredient.unitId ?? ''}`\n const current = lines.get(key)\n if (current) current.quantity = round4(current.quantity + quantity)\n else lines.set(key, { productId: ingredient.productId, locationId: ingredient.locationId, quantity, unitId: ingredient.unitId })\n }\n return [...lines.values()]\n}\n"]}
@@ -0,0 +1,33 @@
1
+ // src/domain/consumption.ts
2
+ function consumptionKey(sourceType, sourceId) {
3
+ return `${sourceType.trim()}:${sourceId.trim()}`;
4
+ }
5
+ function assertSource(sourceType, sourceId) {
6
+ if (!sourceType.trim()) throw new Error("Inventory consumption requires a source type");
7
+ if (!sourceId.trim()) throw new Error("Inventory consumption requires a source id");
8
+ }
9
+ function assertConsumable(input) {
10
+ assertSource(input.sourceType, input.sourceId);
11
+ if (!input.productId) throw new Error("Inventory consumption requires a product id");
12
+ if (!Number.isFinite(input.quantity) || input.quantity <= 0) {
13
+ throw new Error("Inventory consumption quantity must be positive");
14
+ }
15
+ }
16
+ function assertReversible(input) {
17
+ assertSource(input.sourceType, input.sourceId);
18
+ if (!input.reason.trim()) throw new Error("Inventory consumption reversal requires a reason");
19
+ }
20
+ function skippedConsumption(input, skipReason) {
21
+ return {
22
+ sourceType: input.sourceType,
23
+ sourceId: input.sourceId,
24
+ idempotencyKey: consumptionKey(input.sourceType, input.sourceId),
25
+ status: "skipped",
26
+ lineCount: 0,
27
+ skipReason
28
+ };
29
+ }
30
+
31
+ export { assertConsumable, assertReversible, consumptionKey, skippedConsumption };
32
+ //# sourceMappingURL=chunk-GBIFZCEN.js.map
33
+ //# sourceMappingURL=chunk-GBIFZCEN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domain/consumption.ts"],"names":[],"mappings":";AAgCO,SAAS,cAAA,CAAe,YAAoB,QAAA,EAA0B;AAC3E,EAAA,OAAO,GAAG,UAAA,CAAW,IAAA,EAAM,CAAA,CAAA,EAAI,QAAA,CAAS,MAAM,CAAA,CAAA;AAChD;AAEA,SAAS,YAAA,CAAa,YAAoB,QAAA,EAAwB;AAChE,EAAA,IAAI,CAAC,UAAA,CAAW,IAAA,IAAQ,MAAM,IAAI,MAAM,8CAA8C,CAAA;AACtF,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,IAAQ,MAAM,IAAI,MAAM,4CAA4C,CAAA;AACpF;AAEO,SAAS,iBAAiB,KAAA,EAA0C;AACzE,EAAA,YAAA,CAAa,KAAA,CAAM,UAAA,EAAY,KAAA,CAAM,QAAQ,CAAA;AAC7C,EAAA,IAAI,CAAC,KAAA,CAAM,SAAA,EAAW,MAAM,IAAI,MAAM,6CAA6C,CAAA;AACnF,EAAA,IAAI,CAAC,OAAO,QAAA,CAAS,KAAA,CAAM,QAAQ,CAAA,IAAK,KAAA,CAAM,YAAY,CAAA,EAAG;AAC3D,IAAA,MAAM,IAAI,MAAM,iDAAiD,CAAA;AAAA,EACnE;AACF;AAEO,SAAS,iBAAiB,KAAA,EAA4C;AAC3E,EAAA,YAAA,CAAa,KAAA,CAAM,UAAA,EAAY,KAAA,CAAM,QAAQ,CAAA;AAC7C,EAAA,IAAI,CAAC,MAAM,MAAA,CAAO,IAAA,IAAQ,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAC9F;AAEO,SAAS,kBAAA,CACd,OACA,UAAA,EAC4B;AAC5B,EAAA,OAAO;AAAA,IACL,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,cAAA,EAAgB,cAAA,CAAe,KAAA,CAAM,UAAA,EAAY,MAAM,QAAQ,CAAA;AAAA,IAC/D,MAAA,EAAQ,SAAA;AAAA,IACR,SAAA,EAAW,CAAA;AAAA,IACX;AAAA,GACF;AACF","file":"chunk-GBIFZCEN.js","sourcesContent":["/** A business line that consumes the active recipe of one catalogue product. */\nexport interface ConsumeRecipeForSourceInput {\n /** Stable vocabulary owned by the caller, for example `order_item`. */\n sourceType: string\n /** Stable source-line id. It is also the exactly-once boundary. */\n sourceId: string\n /** Product produced by the recipe (the item that was sold). */\n productId: string\n /** Number of recipe outputs consumed by this source line. */\n quantity: number\n}\n\nexport interface ReverseRecipeConsumptionInput {\n sourceType: string\n sourceId: string\n reason: string\n}\n\nexport type InventoryConsumptionStatus = 'confirmed' | 'reversed' | 'skipped'\nexport type InventoryConsumptionSkipReason = 'no_active_recipe' | 'nothing_to_reverse'\n\nexport interface InventoryConsumptionResult {\n sourceType: string\n sourceId: string\n idempotencyKey: string\n status: InventoryConsumptionStatus\n recipeId?: string\n lineCount: number\n skipReason?: InventoryConsumptionSkipReason\n}\n\n/** The source line is the idempotency boundary: replaying it never moves stock twice. */\nexport function consumptionKey(sourceType: string, sourceId: string): string {\n return `${sourceType.trim()}:${sourceId.trim()}`\n}\n\nfunction assertSource(sourceType: string, sourceId: string): void {\n if (!sourceType.trim()) throw new Error('Inventory consumption requires a source type')\n if (!sourceId.trim()) throw new Error('Inventory consumption requires a source id')\n}\n\nexport function assertConsumable(input: ConsumeRecipeForSourceInput): void {\n assertSource(input.sourceType, input.sourceId)\n if (!input.productId) throw new Error('Inventory consumption requires a product id')\n if (!Number.isFinite(input.quantity) || input.quantity <= 0) {\n throw new Error('Inventory consumption quantity must be positive')\n }\n}\n\nexport function assertReversible(input: ReverseRecipeConsumptionInput): void {\n assertSource(input.sourceType, input.sourceId)\n if (!input.reason.trim()) throw new Error('Inventory consumption reversal requires a reason')\n}\n\nexport function skippedConsumption(\n input: Pick<ConsumeRecipeForSourceInput, 'sourceType' | 'sourceId'>,\n skipReason: InventoryConsumptionSkipReason,\n): InventoryConsumptionResult {\n return {\n sourceType: input.sourceType,\n sourceId: input.sourceId,\n idempotencyKey: consumptionKey(input.sourceType, input.sourceId),\n status: 'skipped',\n lineCount: 0,\n skipReason,\n }\n}\n"]}
@@ -0,0 +1,14 @@
1
+ // src/domain/query.ts
2
+ function paginate(items, page = 1, pageSize = 50) {
3
+ const start = (page - 1) * pageSize;
4
+ return { data: items.slice(start, start + pageSize), total: items.length };
5
+ }
6
+ function isoDayFrom(from, days = 0) {
7
+ const d = new Date(from);
8
+ d.setDate(d.getDate() + days);
9
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
10
+ }
11
+
12
+ export { isoDayFrom, paginate };
13
+ //# sourceMappingURL=chunk-GDEIBHAB.js.map
14
+ //# sourceMappingURL=chunk-GDEIBHAB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domain/query.ts"],"names":[],"mappings":";AAUO,SAAS,QAAA,CAAY,KAAA,EAAY,IAAA,GAAO,CAAA,EAAG,WAAW,EAAA,EAAwB;AACnF,EAAA,MAAM,KAAA,GAAA,CAAS,OAAO,CAAA,IAAK,QAAA;AAC3B,EAAA,OAAO,EAAE,IAAA,EAAM,KAAA,CAAM,KAAA,CAAM,KAAA,EAAO,QAAQ,QAAQ,CAAA,EAAG,KAAA,EAAO,KAAA,CAAM,MAAA,EAAO;AAC3E;AAGO,SAAS,UAAA,CAAW,IAAA,EAAY,IAAA,GAAO,CAAA,EAAW;AACvD,EAAA,MAAM,CAAA,GAAI,IAAI,IAAA,CAAK,IAAI,CAAA;AACvB,EAAA,CAAA,CAAE,OAAA,CAAQ,CAAA,CAAE,OAAA,EAAQ,GAAI,IAAI,CAAA;AAC5B,EAAA,OAAO,CAAA,EAAG,CAAA,CAAE,WAAA,EAAa,CAAA,CAAA,EAAI,OAAO,CAAA,CAAE,QAAA,EAAS,GAAI,CAAC,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,CAAA,CAAE,OAAA,EAAS,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAA;AAChH","file":"chunk-GDEIBHAB.js","sourcesContent":["export interface DateRange {\n from: string\n to: string\n}\n\nexport interface PaginatedResult<T> {\n data: T[]\n total: number\n}\n\nexport function paginate<T>(items: T[], page = 1, pageSize = 50): PaginatedResult<T> {\n const start = (page - 1) * pageSize\n return { data: items.slice(start, start + pageSize), total: items.length }\n}\n\n/** `YYYY-MM-DD` in local time, `days` from `from`. */\nexport function isoDayFrom(from: Date, days = 0): string {\n const d = new Date(from)\n d.setDate(d.getDate() + days)\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`\n}\n"]}
@@ -0,0 +1,35 @@
1
+ // src/domain/product.ts
2
+ function derivedProductType(c, fallback) {
3
+ const otherwise = fallback ?? "sale";
4
+ if (!c) return otherwise;
5
+ if (c.isForSale) return "sale";
6
+ if (c.isIngredient) return "ingredient";
7
+ if (c.isIntermediate) return "intermediate";
8
+ if (c.isAsset) return "asset";
9
+ return otherwise;
10
+ }
11
+ function marginPercent(salePrice, costPrice) {
12
+ const sale = Number(salePrice) || 0;
13
+ const cost = Number(costPrice) || 0;
14
+ if (sale <= 0 || cost <= 0) return null;
15
+ return (sale - cost) / cost * 100;
16
+ }
17
+ function stockValue(quantity, costPrice) {
18
+ const qty = Number(quantity) || 0;
19
+ const cost = Number(costPrice) || 0;
20
+ if (qty > 0 && cost === 0) return null;
21
+ return qty * cost;
22
+ }
23
+ var PRODUCT_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
24
+ function productImageProblem(file) {
25
+ if (!file.type.startsWith("image/")) return "Selecione um arquivo de imagem.";
26
+ if (file.size > PRODUCT_IMAGE_MAX_BYTES) return "A foto deve ter no m\xE1ximo 8 MB.";
27
+ return void 0;
28
+ }
29
+ function isNewBarcodePrimary(input, existingCount) {
30
+ return input.isPrimary ?? existingCount === 0;
31
+ }
32
+
33
+ export { PRODUCT_IMAGE_MAX_BYTES, derivedProductType, isNewBarcodePrimary, marginPercent, productImageProblem, stockValue };
34
+ //# sourceMappingURL=chunk-JN5AZ656.js.map
35
+ //# sourceMappingURL=chunk-JN5AZ656.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domain/product.ts"],"names":[],"mappings":";AAyOO,SAAS,kBAAA,CAAmB,GAA+C,QAAA,EAAgC;AAChH,EAAA,MAAM,YAAa,QAAA,IAAwC,MAAA;AAC3D,EAAA,IAAI,CAAC,GAAG,OAAO,SAAA;AACf,EAAA,IAAI,CAAA,CAAE,WAAW,OAAO,MAAA;AACxB,EAAA,IAAI,CAAA,CAAE,cAAc,OAAO,YAAA;AAC3B,EAAA,IAAI,CAAA,CAAE,gBAAgB,OAAO,cAAA;AAC7B,EAAA,IAAI,CAAA,CAAE,SAAS,OAAO,OAAA;AACtB,EAAA,OAAO,SAAA;AACT;AAGO,SAAS,aAAA,CAAc,WAAmB,SAAA,EAAkC;AACjF,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,SAAS,CAAA,IAAK,CAAA;AAClC,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,SAAS,CAAA,IAAK,CAAA;AAClC,EAAA,IAAI,IAAA,IAAQ,CAAA,IAAK,IAAA,IAAQ,CAAA,EAAG,OAAO,IAAA;AACnC,EAAA,OAAA,CAAS,IAAA,GAAO,QAAQ,IAAA,GAAQ,GAAA;AAClC;AAGO,SAAS,UAAA,CAAW,UAAkB,SAAA,EAAkC;AAC7E,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,QAAQ,CAAA,IAAK,CAAA;AAChC,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,SAAS,CAAA,IAAK,CAAA;AAClC,EAAA,IAAI,GAAA,GAAM,CAAA,IAAK,IAAA,KAAS,CAAA,EAAG,OAAO,IAAA;AAClC,EAAA,OAAO,GAAA,GAAM,IAAA;AACf;AAEO,IAAM,uBAAA,GAA0B,IAAI,IAAA,GAAO;AAG3C,SAAS,oBAAoB,IAAA,EAA0D;AAC5F,EAAA,IAAI,CAAC,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,QAAQ,GAAG,OAAO,iCAAA;AAC5C,EAAA,IAAI,IAAA,CAAK,IAAA,GAAO,uBAAA,EAAyB,OAAO,oCAAA;AAChD,EAAA,OAAO,MAAA;AACT;AAGO,SAAS,mBAAA,CAAoB,OAA0C,aAAA,EAAgC;AAC5G,EAAA,OAAO,KAAA,CAAM,aAAa,aAAA,KAAkB,CAAA;AAC9C","file":"chunk-JN5AZ656.js","sourcesContent":["export type ProductType = 'ingredient' | 'sale' | 'intermediate' | 'asset'\n\nexport type ProductPurpose = 'purchase' | 'sale' | 'both' | 'internal'\n\n/** The seven independent switches; a product can be several things at once, each one opening its own ficha. */\nexport interface ProductClassification {\n isIngredient: boolean\n isForSale: boolean\n isIntermediate: boolean\n isAsset: boolean\n isProduced: boolean\n isResale: boolean\n trackByUnitLabel: boolean\n}\n\nexport interface ProductIngredientInfo {\n minimumQuantity?: number\n maximumQuantity?: number\n /** Sits above the minimum: the balance at which buying should start. */\n reorderPoint?: number\n leadTimeDays?: number\n shelfLifeDays?: number\n storageTemperature?: string\n allowedSupplierIds?: string[]\n notes?: string\n}\n\nexport interface ProductSaleInfo {\n menuDescription?: string\n allergens?: string\n preparationTimeMinutes?: number\n servesQuantity?: number\n calories?: number\n soldByWeight?: boolean\n highlightDelivery?: boolean\n availableForDelivery?: boolean\n availableForCounter?: boolean\n notes?: string\n}\n\nexport interface ProductSaleChannel {\n channel: string\n price: number\n active?: boolean\n}\n\nexport interface ProductIntermediateInfo {\n yieldPercentage?: number\n preparationTimeMinutes?: number\n shelfLifeHours?: number\n batchSize?: number\n batchUnitId?: string\n storageInstructions?: string\n notes?: string\n}\n\nexport interface ProductAssetInfo {\n assetValue?: number\n residualValue?: number\n usefulLifeMonths?: number\n depreciationRate?: number\n serialNumber?: string\n acquisitionDate?: string\n warrantyExpiryDate?: string\n assetStatus?: string\n assetLocation?: string\n responsiblePersonId?: string\n insurancePolicy?: string\n lastMaintenanceDate?: string\n nextMaintenanceDate?: string\n notes?: string\n}\n\n/** The product's exception to its tax group; `undefined` means \"whatever the group says\", never zero. */\nexport interface ProductTaxInfo {\n taxGroupId?: string\n ncm?: string\n cest?: string\n icmsOrigin?: string\n icmsRate?: number\n cfop?: string\n cst?: string\n pisSituation?: string\n pisRate?: number\n cofinsSituation?: string\n cofinsRate?: number\n baseCalculationMode?: string\n benefitCode?: string\n povertyFundRate?: number\n ibsCbsSituation?: string\n ibsCbsClassification?: string\n}\n\nexport interface ProductBarcode {\n id?: string\n barcode: string\n barcodeType?: string\n description?: string\n isPrimary?: boolean\n}\n\n/** One sellable form of a product (size, colour). The default one stands for a product with none. */\nexport interface ProductVariant {\n id: string\n name: string\n sku?: string\n isDefault: boolean\n isActive: boolean\n /** Units in stock across the units the person can see. */\n onHand: number\n}\n\nexport interface ProductGeneralExtras {\n commissionPercent?: number\n chartAccountId?: string\n /** How much of `contentUnitId` fits in one stock unit. */\n contentPerUnit?: number\n contentUnitId?: string\n avgPurchasePrice?: number\n avgPurchasePriceUpdatedAt?: string\n professionalResalePrice?: number\n requireBatchForStock?: boolean\n requireExpiryForStock?: boolean\n}\n\nexport interface Product {\n id: string\n name: string\n description?: string\n sku?: string\n barcode?: string\n brand?: string\n categoryId?: string\n categoryName?: string\n /** Derived from `classification`; see `derivedProductType`. */\n productType: ProductType\n purpose?: ProductPurpose\n classification?: ProductClassification\n ingredientInfo?: ProductIngredientInfo\n saleInfo?: ProductSaleInfo\n saleChannels?: ProductSaleChannel[]\n intermediateInfo?: ProductIntermediateInfo\n assetInfo?: ProductAssetInfo\n taxInfo?: ProductTaxInfo\n currentQuantity: number\n minQuantity: number\n maxQuantity?: number\n costPrice: number\n salePrice?: number\n measurementUnitId?: string\n measurementUnitName?: string\n purchaseUnitId?: string\n /** How many measurement units fit in one purchase unit. 1 when they are the same. */\n conversionFactor?: number\n isActive: boolean\n supplierId?: string\n supplierName?: string\n defaultLocationId?: string\n imageUrl?: string\n extras?: ProductGeneralExtras\n metadata?: Record<string, unknown>\n tenantId: string\n createdAt: string\n updatedAt: string\n}\n\nexport interface ProductCategory {\n id: string\n name: string\n parentId?: string\n isActive: boolean\n tenantId: string\n createdAt: string\n updatedAt: string\n}\n\nexport interface MeasurementUnit {\n id: string\n name: string\n abbreviation: string\n isActive: boolean\n tenantId: string\n createdAt: string\n}\n\nexport interface Supplier {\n id: string\n name: string\n documentNumber?: string\n phone?: string\n}\n\nexport interface CreateProductInput {\n name: string\n description?: string\n sku?: string\n barcode?: string\n brand?: string\n categoryId?: string\n productType: ProductType\n purpose?: ProductPurpose\n minQuantity?: number\n maxQuantity?: number\n costPrice?: number\n salePrice?: number\n measurementUnitId?: string\n purchaseUnitId?: string\n conversionFactor?: number\n supplierId?: string\n defaultLocationId?: string\n imageUrl?: string\n classification?: Partial<ProductClassification>\n extras?: ProductGeneralExtras\n ingredientInfo?: ProductIngredientInfo\n saleInfo?: ProductSaleInfo\n saleChannels?: ProductSaleChannel[]\n intermediateInfo?: ProductIntermediateInfo\n assetInfo?: ProductAssetInfo\n taxInfo?: ProductTaxInfo\n metadata?: Record<string, unknown>\n}\n\nexport interface ProductQuery {\n productType?: ProductType\n categoryId?: string\n search?: string\n lowStockOnly?: boolean\n isActive?: boolean\n page?: number\n pageSize?: number\n}\n\n/** The one type the list facet shows; sale wins because that is what the counter calls the thing. */\nexport function derivedProductType(c: Partial<ProductClassification> | undefined, fallback?: string): ProductType {\n const otherwise = (fallback as ProductType | undefined) ?? 'sale'\n if (!c) return otherwise\n if (c.isForSale) return 'sale'\n if (c.isIngredient) return 'ingredient'\n if (c.isIntermediate) return 'intermediate'\n if (c.isAsset) return 'asset'\n return otherwise\n}\n\n/** Markup over cost, in percent. Null when either price is not filled in: zero means \"not registered\", not \"free\". */\nexport function marginPercent(salePrice: number, costPrice: number): number | null {\n const sale = Number(salePrice) || 0\n const cost = Number(costPrice) || 0\n if (sale <= 0 || cost <= 0) return null\n return ((sale - cost) / cost) * 100\n}\n\n/** On-hand × cost. Null when there is stock but no cost: the value is unknown, not zero. */\nexport function stockValue(quantity: number, costPrice: number): number | null {\n const qty = Number(quantity) || 0\n const cost = Number(costPrice) || 0\n if (qty > 0 && cost === 0) return null\n return qty * cost\n}\n\nexport const PRODUCT_IMAGE_MAX_BYTES = 8 * 1024 * 1024\n\n/** Why a file cannot be a product photo, in the words the screen shows; undefined when it can. */\nexport function productImageProblem(file: { type: string; size: number }): string | undefined {\n if (!file.type.startsWith('image/')) return 'Selecione um arquivo de imagem.'\n if (file.size > PRODUCT_IMAGE_MAX_BYTES) return 'A foto deve ter no máximo 8 MB.'\n return undefined\n}\n\n/** The first barcode a product gets is its primary one unless the caller says otherwise. */\nexport function isNewBarcodePrimary(input: Pick<ProductBarcode, 'isPrimary'>, existingCount: number): boolean {\n return input.isPrimary ?? existingCount === 0\n}\n\n/** A choice group of a product sold (Complementos): «Ponto da carne». */\nexport interface ProductOptionGroup {\n id?: string\n name: string\n isActive: boolean\n isRequired: boolean\n minSelections: number\n maxSelections: number\n items: ProductOptionItem[]\n}\n\nexport interface ProductOptionItem {\n id?: string\n name: string\n priceAdjustment: number\n isActive: boolean\n}\n"]}
@@ -0,0 +1,15 @@
1
+ // src/lib/idempotency.ts
2
+ function randomId() {
3
+ try {
4
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
5
+ } catch {
6
+ }
7
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
8
+ }
9
+ function createIdempotencyKey(scope) {
10
+ return `${scope}:${randomId()}`;
11
+ }
12
+
13
+ export { createIdempotencyKey, randomId };
14
+ //# sourceMappingURL=chunk-JX7NCXJQ.js.map
15
+ //# sourceMappingURL=chunk-JX7NCXJQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/idempotency.ts"],"names":[],"mappings":";AAAO,SAAS,QAAA,GAAmB;AACjC,EAAA,IAAI;AACF,IAAA,IAAI,OAAO,WAAW,MAAA,EAAQ,UAAA,KAAe,YAAY,OAAO,UAAA,CAAW,OAAO,UAAA,EAAW;AAAA,EAC/F,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,OAAO,GAAG,IAAA,CAAK,GAAA,EAAI,CAAE,QAAA,CAAS,EAAE,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,MAAA,GAAS,QAAA,CAAS,EAAE,EAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA,CAAA;AAC9E;AAGO,SAAS,qBAAqB,KAAA,EAAuB;AAC1D,EAAA,OAAO,CAAA,EAAG,KAAK,CAAA,CAAA,EAAI,QAAA,EAAU,CAAA,CAAA;AAC/B","file":"chunk-JX7NCXJQ.js","sourcesContent":["export function randomId(): string {\n try {\n if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID()\n } catch {\n // randomUUID throws outside a secure context; the fallback below is enough for a key.\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n}\n\n/** Created once per operator action and reused on every retry of it. */\nexport function createIdempotencyKey(scope: string): string {\n return `${scope}:${randomId()}`\n}\n"]}