@molecule/api-resource-invoice 1.0.0 → 1.0.2

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 (2) hide show
  1. package/README.md +370 -0
  2. package/package.json +14 -7
package/README.md ADDED
@@ -0,0 +1,370 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-04T01:49:14.427Z
7
+ -->
8
+
9
+ # @molecule/api-resource-invoice
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ `@molecule/api-resource-invoice` — line-item-based invoice CRUD with
16
+ auto-computed totals (subtotal + tax), draft/sent/partial/paid/overdue/void
17
+ status, and a `recordPayment(invoiceId, userId, amount)` helper that
18
+ advances status automatically.
19
+
20
+ Extracted from the invoice-billing flagship.
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import { createInvoiceRouter } from '@molecule/api-resource-invoice'
26
+ app.use('/invoices', createInvoiceRouter())
27
+ ```
28
+
29
+ ```ts
30
+ import { createInvoiceForUser, recordPayment } from '@molecule/api-resource-invoice'
31
+
32
+ const inv = await createInvoiceForUser(userId, {
33
+ client_id: 'acme-co',
34
+ items: [{ description: 'Consulting', quantity: 10, unit_price: 250 }],
35
+ tax_rate: 8.5,
36
+ })
37
+ await recordPayment(inv.id, userId, 2710.0) // marks paid
38
+ ```
39
+
40
+ ## Type
41
+
42
+ `resource`
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ npm install @molecule/api-resource-invoice @molecule/api-bonds-default-express @molecule/api-database @molecule/api-i18n @molecule/api-middleware-validation express zod
48
+ npm install -D @types/express
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### Interfaces
54
+
55
+ #### `Invoice`
56
+
57
+ Normalized invoice with all date fields serialized to ISO strings for API responses.
58
+
59
+ ```typescript
60
+ interface Invoice extends Omit<
61
+ InvoiceRow,
62
+ 'issue_date' | 'due_date' | 'paid_at' | 'created_at' | 'updated_at'
63
+ > {
64
+ issue_date: string
65
+ due_date: string | null
66
+ paid_at: string | null
67
+ created_at: string
68
+ updated_at: string
69
+ }
70
+ ```
71
+
72
+ #### `InvoiceRow`
73
+
74
+ Raw database row shape for an invoice, with date fields typed as string or Date.
75
+
76
+ ```typescript
77
+ interface InvoiceRow {
78
+ id: string
79
+ user_id: string
80
+ client_id: string
81
+ number: string
82
+ status: InvoiceStatus
83
+ items: LineItem[]
84
+ subtotal: number
85
+ tax_rate: number
86
+ tax_amount: number
87
+ total: number
88
+ amount_paid: number
89
+ currency: string
90
+ issue_date: string | Date
91
+ due_date: string | Date | null
92
+ paid_at: string | Date | null
93
+ notes: string | null
94
+ created_at: string | Date
95
+ updated_at: string | Date
96
+ }
97
+ ```
98
+
99
+ #### `LineItem`
100
+
101
+ A single billable line on an invoice, with description, quantity, and unit price.
102
+
103
+ ```typescript
104
+ interface LineItem {
105
+ description: string
106
+ quantity: number
107
+ unit_price: number
108
+ }
109
+ ```
110
+
111
+ ### Types
112
+
113
+ #### `InvoiceStatus`
114
+
115
+ Lifecycle states an invoice can be in, from initial draft through payment or cancellation.
116
+
117
+ ```typescript
118
+ type InvoiceStatus = 'draft' | 'sent' | 'partial' | 'paid' | 'overdue' | 'void'
119
+ ```
120
+
121
+ ### Functions
122
+
123
+ #### `computeTotals(items, taxRate)`
124
+
125
+ Compute subtotal/tax/total from line items + tax rate (percent 0-100).
126
+
127
+ ```typescript
128
+ function computeTotals(
129
+ items: LineItem[],
130
+ taxRate: number,
131
+ ): { subtotal: number; tax_amount: number; total: number }
132
+ ```
133
+
134
+ #### `createInvoiceForUser(userId, data)`
135
+
136
+ Create a new draft invoice for a user, computing totals from line items and tax rate.
137
+
138
+ ```typescript
139
+ function createInvoiceForUser(
140
+ userId: string,
141
+ data: {
142
+ client_id: string
143
+ items: LineItem[]
144
+ due_date?: string
145
+ notes?: string
146
+ tax_rate?: number
147
+ currency?: string
148
+ },
149
+ ): Promise<Invoice>
150
+ ```
151
+
152
+ #### `createInvoiceRouter()`
153
+
154
+ Creates and returns an Express router with all CRUD + payment routes for invoices.
155
+
156
+ ```typescript
157
+ function createInvoiceRouter(): Router
158
+ ```
159
+
160
+ #### `deleteInvoiceForUser(invoiceId, userId)`
161
+
162
+ Delete a user-owned invoice by ID, returning false if not found or not owned by the user.
163
+
164
+ ```typescript
165
+ function deleteInvoiceForUser(invoiceId: string, userId: string): Promise<boolean>
166
+ ```
167
+
168
+ #### `getInvoiceForUser(invoiceId, userId)`
169
+
170
+ Fetch a single invoice by ID, returning null if not found or not owned by the user.
171
+
172
+ ```typescript
173
+ function getInvoiceForUser(invoiceId: string, userId: string): Promise<Invoice | null>
174
+ ```
175
+
176
+ #### `listInvoicesForUser(userId, opts?)`
177
+
178
+ List all invoices for a user, with optional client/status filters and pagination.
179
+
180
+ ```typescript
181
+ function listInvoicesForUser(
182
+ userId: string,
183
+ opts?: { client_id?: string; status?: InvoiceStatus; page?: number; limit?: number },
184
+ ): Promise<{ data: Invoice[]; total: number; page: number; limit: number }>
185
+ ```
186
+
187
+ #### `recordPayment(invoiceId, userId, amount)`
188
+
189
+ Record a payment amount against an invoice, updating amount_paid and transitioning status to partial or paid.
190
+
191
+ ```typescript
192
+ function recordPayment(invoiceId: string, userId: string, amount: number): Promise<Invoice | null>
193
+ ```
194
+
195
+ #### `toInvoice(row)`
196
+
197
+ Map a raw database InvoiceRow to a normalized Invoice with ISO date strings.
198
+
199
+ ```typescript
200
+ function toInvoice(row: InvoiceRow): Invoice
201
+ ```
202
+
203
+ #### `updateInvoiceForUser(invoiceId, userId, patch)`
204
+
205
+ Apply a partial update to a user-owned invoice, recomputing totals and marking paid_at when status becomes paid.
206
+
207
+ ```typescript
208
+ function updateInvoiceForUser(
209
+ invoiceId: string,
210
+ userId: string,
211
+ patch: Partial<{
212
+ items: LineItem[]
213
+ due_date: string
214
+ notes: string
215
+ tax_rate: number
216
+ currency: string
217
+ status: InvoiceStatus
218
+ }>,
219
+ ): Promise<Invoice | null>
220
+ ```
221
+
222
+ ### Constants
223
+
224
+ #### `INVOICE_STATUSES`
225
+
226
+ All valid lifecycle statuses an invoice can hold.
227
+
228
+ ```typescript
229
+ const INVOICE_STATUSES: readonly ['draft', 'sent', 'partial', 'paid', 'overdue', 'void']
230
+ ```
231
+
232
+ #### `invoiceCreateSchema`
233
+
234
+ Zod schema for creating a new invoice (client, items, optional due date / notes / tax / currency).
235
+
236
+ ```typescript
237
+ const invoiceCreateSchema: z.ZodObject<
238
+ {
239
+ client_id: z.ZodString
240
+ items: z.ZodArray<
241
+ z.ZodObject<
242
+ { description: z.ZodString; quantity: z.ZodNumber; unit_price: z.ZodNumber },
243
+ z.core.$strip
244
+ >
245
+ >
246
+ due_date: z.ZodOptional<z.ZodString>
247
+ notes: z.ZodOptional<z.ZodString>
248
+ tax_rate: z.ZodOptional<z.ZodNumber>
249
+ currency: z.ZodOptional<z.ZodString>
250
+ },
251
+ z.core.$strip
252
+ >
253
+ ```
254
+
255
+ #### `invoiceUpdateSchema`
256
+
257
+ Zod schema for partially updating an existing invoice (all fields optional).
258
+
259
+ ```typescript
260
+ const invoiceUpdateSchema: z.ZodObject<
261
+ {
262
+ items: z.ZodOptional<
263
+ z.ZodArray<
264
+ z.ZodObject<
265
+ { description: z.ZodString; quantity: z.ZodNumber; unit_price: z.ZodNumber },
266
+ z.core.$strip
267
+ >
268
+ >
269
+ >
270
+ due_date: z.ZodOptional<z.ZodString>
271
+ notes: z.ZodOptional<z.ZodString>
272
+ tax_rate: z.ZodOptional<z.ZodNumber>
273
+ currency: z.ZodOptional<z.ZodString>
274
+ status: z.ZodOptional<
275
+ z.ZodEnum<{
276
+ draft: 'draft'
277
+ sent: 'sent'
278
+ partial: 'partial'
279
+ paid: 'paid'
280
+ overdue: 'overdue'
281
+ void: 'void'
282
+ }>
283
+ >
284
+ },
285
+ z.core.$strip
286
+ >
287
+ ```
288
+
289
+ #### `lineItemSchema`
290
+
291
+ Zod schema for a single invoice line item (description, quantity, unit price).
292
+
293
+ ```typescript
294
+ const lineItemSchema: z.ZodObject<
295
+ { description: z.ZodString; quantity: z.ZodNumber; unit_price: z.ZodNumber },
296
+ z.core.$strip
297
+ >
298
+ ```
299
+
300
+ #### `recordPaymentSchema`
301
+
302
+ Zod schema for recording a payment against an invoice (positive amount required).
303
+
304
+ ```typescript
305
+ const recordPaymentSchema: z.ZodObject<{ amount: z.ZodNumber }, z.core.$strip>
306
+ ```
307
+
308
+ ## Injection Notes
309
+
310
+ ### Requirements
311
+
312
+ Peer dependencies:
313
+
314
+ - `@molecule/api-bonds-default-express` ^1.0.1
315
+ - `@molecule/api-database` ^1.0.1
316
+ - `@molecule/api-i18n` ^1.0.1
317
+ - `@molecule/api-middleware-validation` ^1.0.1
318
+ - `express` ^5.0.0
319
+ - `zod` ^4.0.0
320
+
321
+ ### Runtime Dependencies
322
+
323
+ - `@molecule/api-bonds-default-express`
324
+ - `@molecule/api-database`
325
+ - `@molecule/api-i18n`
326
+ - `@molecule/api-middleware-validation`
327
+ - `express`
328
+ - `zod`
329
+
330
+ Table: `src/__setup__/invoices.sql` creates the single `invoices` table. An
331
+ mlcl-scaffolded API replays `__setup__/*.sql` automatically on migrate;
332
+ anywhere else run it once. The bundled `computeTotals(items, taxRate)`
333
+ helper is exported for client-side total previews.
334
+
335
+ Everything is owner-scoped: service functions take the authenticated
336
+ `userId` and return `null`/`false` for rows the caller doesn't own, and the
337
+ router reads the caller from `res.locals.session` (mount behind your global
338
+ auth middleware; 401 otherwise). `recordPayment` is bookkeeping — "record a
339
+ payment I received" against my own invoice — it never talks to a payment
340
+ provider; wire actual charging separately (see `@molecule/api-payments`).
341
+
342
+ ## E2E Tests
343
+
344
+ Integration checklist — drive the real UI (live preview, no mocks), adapt
345
+ each item to this app's actual screens/flows, and check every box off one
346
+ by one. A box you can't check is an integration bug to fix — not a skip:
347
+
348
+ - [ ] Creating an invoice with line items auto-computes the money: add a
349
+ line (quantity × unit_price) and the subtotal updates, the tax_rate is
350
+ applied (tax_amount = subtotal × rate / 100), and total = subtotal +
351
+ tax_amount — matching `computeTotals` down to the rounded cents shown in
352
+ the UI. Change a quantity or add another line and every figure re-computes.
353
+ - [ ] The status lifecycle advances correctly through INVOICE_STATUSES: a
354
+ freshly created invoice reads `draft`; sending it moves it to `sent`; a
355
+ `recordPayment` for LESS than the balance flips it to `partial`; paying the
356
+ remaining balance flips it to `paid` and stamps a paid date. The
357
+ balance-due shown to the user (total - amount_paid) is correct after each
358
+ step.
359
+ - [ ] Terminal/edge states read right: an unpaid invoice past its due_date
360
+ shows `overdue`, a cancelled one shows `void`, and once an invoice is `paid`
361
+ or `void` the UI blocks further line-item/total edits as designed.
362
+ - [ ] Overpayment and dead-invoice payments are rejected, not silently
363
+ over-applied: recording an amount greater than the outstanding balance, or
364
+ any payment against an already-`paid` or `void` invoice, fails with a
365
+ visible error and leaves amount_paid + status unchanged.
366
+ - [ ] AUTHORIZATION: a signed-in user sees and mutates only THEIR OWN
367
+ invoices — every path is `*ForUser`-scoped (listInvoicesForUser /
368
+ getInvoiceForUser / updateInvoiceForUser / recordPayment). Guessing or
369
+ tampering another user's invoice id on GET/PUT/DELETE/:id/payment returns
370
+ 403/404 — never that invoice's data and never a cross-user payment.
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@molecule/api-resource-invoice",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Invoice CRUD + line items + draft/sent/paid/overdue status. Extracted from invoice-billing flagship.",
5
+ "homepage": "https://www.molecule.dev/packages/api-resource-invoice",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
@@ -17,7 +18,8 @@
17
18
  }
18
19
  },
19
20
  "files": [
20
- "dist"
21
+ "dist",
22
+ "README.md"
21
23
  ],
22
24
  "keywords": [
23
25
  "molecule",
@@ -26,19 +28,24 @@
26
28
  "resource"
27
29
  ],
28
30
  "license": "Apache-2.0",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/molecule-dev/molecule.git",
34
+ "directory": "packages/api/resources/invoice"
35
+ },
29
36
  "devDependencies": {
30
37
  "@types/express": "5.0.6",
31
38
  "@types/node": "26.1.2",
32
39
  "express": "5.2.1",
33
40
  "typescript": "6.0.3",
34
- "vitest": "4.1.10",
41
+ "vitest": "4.1.11",
35
42
  "zod": "4.4.3"
36
43
  },
37
44
  "peerDependencies": {
38
- "@molecule/api-bonds-default-express": "^1.0.0",
39
- "@molecule/api-database": "^1.0.0",
40
- "@molecule/api-i18n": "^1.0.0",
41
- "@molecule/api-middleware-validation": "^1.0.0",
45
+ "@molecule/api-bonds-default-express": "^1.0.1",
46
+ "@molecule/api-database": "^1.0.1",
47
+ "@molecule/api-i18n": "^1.0.1",
48
+ "@molecule/api-middleware-validation": "^1.0.1",
42
49
  "express": "^5.0.0",
43
50
  "zod": "^4.0.0"
44
51
  }