@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.
- package/README.md +370 -0
- 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.
|
|
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.
|
|
41
|
+
"vitest": "4.1.11",
|
|
35
42
|
"zod": "4.4.3"
|
|
36
43
|
},
|
|
37
44
|
"peerDependencies": {
|
|
38
|
-
"@molecule/api-bonds-default-express": "^1.0.
|
|
39
|
-
"@molecule/api-database": "^1.0.
|
|
40
|
-
"@molecule/api-i18n": "^1.0.
|
|
41
|
-
"@molecule/api-middleware-validation": "^1.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
|
}
|