@webbers/invoices-medusa 1.0.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.
- package/.medusa/server/src/admin/index.js +86 -0
- package/.medusa/server/src/admin/index.mjs +85 -0
- package/.medusa/server/src/api/admin/orders/[id]/invoice/[invoice_id]/route.js +28 -0
- package/.medusa/server/src/api/middlewares.js +13 -0
- package/.medusa/server/src/api/store/orders/[id]/invoice/route.js +37 -0
- package/.medusa/server/src/core/classes/pdf-generator/index.js +36 -0
- package/.medusa/server/src/core/classes/pdf-generator/templates/base.js +32 -0
- package/.medusa/server/src/core/classes/pdf-generator/templates/credit-invoice-content.js +308 -0
- package/.medusa/server/src/core/classes/pdf-generator/templates/invoice-content.js +496 -0
- package/.medusa/server/src/core/classes/pdf-generator/templates/packing-slip-content.js +164 -0
- package/.medusa/server/src/i18n/index.js +41 -0
- package/.medusa/server/src/i18n/messages/de.js +80 -0
- package/.medusa/server/src/i18n/messages/en.js +80 -0
- package/.medusa/server/src/i18n/messages/fr.js +80 -0
- package/.medusa/server/src/i18n/messages/it.js +80 -0
- package/.medusa/server/src/i18n/messages/nl.js +80 -0
- package/.medusa/server/src/links/invoice-order.js +17 -0
- package/.medusa/server/src/modules/invoice/index.js +15 -0
- package/.medusa/server/src/modules/invoice/loaders/validate.js +12 -0
- package/.medusa/server/src/modules/invoice/migrations/Migration20260312154721.js +18 -0
- package/.medusa/server/src/modules/invoice/migrations/Migration20260403000001.js +16 -0
- package/.medusa/server/src/modules/invoice/models/invoice.js +14 -0
- package/.medusa/server/src/modules/invoice/service.js +45 -0
- package/.medusa/server/src/subscribers/fulfillment-created-invoice.js +39 -0
- package/.medusa/server/src/subscribers/payment-refunded-invoice.js +65 -0
- package/.medusa/server/src/types/index.js +3 -0
- package/.medusa/server/src/utils/format-locale-amount.js +12 -0
- package/.medusa/server/src/workflows/create-credit-invoice.js +37 -0
- package/.medusa/server/src/workflows/create-invoice.js +38 -0
- package/.medusa/server/src/workflows/generate-invoice-pdf.js +10 -0
- package/.medusa/server/src/workflows/steps/create-invoice-step.js +25 -0
- package/.medusa/server/src/workflows/steps/generate-invoice-pdf-step.js +61 -0
- package/.medusa/server/src/workflows/steps/upload-invoice-pdf-step.js +29 -0
- package/README.md +219 -0
- package/package.json +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# @webbers/invoices-medusa
|
|
2
|
+
|
|
3
|
+
A Medusa v2 plugin that automatically generates sequential PDF invoices for orders and credit notes for refunds. PDFs are uploaded to your private file storage bucket and served to customers and admins via presigned URLs.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Medusa `>= 2.4.0`
|
|
8
|
+
- A configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file) (e.g. S3/R2) with private bucket support
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @webbers/invoices-medusa
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
Register the plugin in `medusa-config.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { defineConfig } from "@medusajs/framework/utils"
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
plugins: [
|
|
25
|
+
{
|
|
26
|
+
resolve: "@webbers/invoices-medusa",
|
|
27
|
+
options: {
|
|
28
|
+
// Optional: locale to fall back to when order.metadata.locale is absent
|
|
29
|
+
// or not a supported value. Defaults to "nl".
|
|
30
|
+
defaultLocale: "en",
|
|
31
|
+
|
|
32
|
+
addressInfo: {
|
|
33
|
+
// Optional: SVG string or base64 data URL shown in the default header
|
|
34
|
+
companyLogo: "<svg>...</svg>",
|
|
35
|
+
// Optional: rendered width of the logo in points (default: 110)
|
|
36
|
+
companyLogoWidth: 110,
|
|
37
|
+
|
|
38
|
+
companyName: "Acme B.V.",
|
|
39
|
+
|
|
40
|
+
// Receives the i18n countries object for the resolved locale so you
|
|
41
|
+
// can include a translated country name in the address block.
|
|
42
|
+
address: (countries) =>
|
|
43
|
+
`Streetname 1\n1234 AB Amsterdam, ${countries.nl}`,
|
|
44
|
+
|
|
45
|
+
cocNumber: "12345678",
|
|
46
|
+
vatNumber: "NL123456789B01",
|
|
47
|
+
iban: "NL00BANK0123456789",
|
|
48
|
+
email: "billing@example.com",
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Optional: accent colors used in table headers
|
|
52
|
+
colors: {
|
|
53
|
+
background: "#004534", // header cell fill (default: #000)
|
|
54
|
+
text: "#ffffff", // header cell text (default: #fff)
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Optional: pdfmake Content rendered as the page header.
|
|
58
|
+
// Overrides the default logo header when provided.
|
|
59
|
+
header: {
|
|
60
|
+
table: {
|
|
61
|
+
widths: ["*"],
|
|
62
|
+
body: [[{ text: "Acme B.V.", fontSize: 18, alignment: "center" }]],
|
|
63
|
+
},
|
|
64
|
+
layout: "noBorders",
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Optional: pdfmake Content rendered as the page footer.
|
|
68
|
+
footer: {
|
|
69
|
+
table: {
|
|
70
|
+
widths: ["*"],
|
|
71
|
+
body: [[{ text: "acme.com", alignment: "center" }]],
|
|
72
|
+
},
|
|
73
|
+
layout: "noBorders",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Configuration reference
|
|
82
|
+
|
|
83
|
+
| Option | Type | Required | Description |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `defaultLocale` | `Locale` | No | Fallback locale when `order.metadata.locale` is absent or invalid. Defaults to `"nl"`. |
|
|
86
|
+
| `addressInfo.companyName` | `string` | Yes | Company name shown on the invoice. |
|
|
87
|
+
| `addressInfo.address` | `(countries: Record<string, string>) => string` | Yes | Function returning the company address. Receives the i18n country name map for the resolved locale. |
|
|
88
|
+
| `addressInfo.cocNumber` | `string` | Yes | Chamber of Commerce number. |
|
|
89
|
+
| `addressInfo.vatNumber` | `string` | Yes | VAT registration number. |
|
|
90
|
+
| `addressInfo.iban` | `string` | Yes | Bank account number. |
|
|
91
|
+
| `addressInfo.email` | `string` | Yes | Billing contact e-mail. |
|
|
92
|
+
| `addressInfo.companyLogo` | `string` | No | SVG string or data URL used in the default header. |
|
|
93
|
+
| `addressInfo.companyLogoWidth` | `number` | No | Rendered logo width in points. |
|
|
94
|
+
| `colors.background` | `string` | No | Table header fill color (CSS hex). |
|
|
95
|
+
| `colors.text` | `string` | No | Table header text color (CSS hex). |
|
|
96
|
+
| `header` | `Content` | No | pdfmake content block rendered as page header. Replaces the default logo header. |
|
|
97
|
+
| `footer` | `Content` | No | pdfmake content block rendered as page footer. |
|
|
98
|
+
|
|
99
|
+
## How it works
|
|
100
|
+
|
|
101
|
+
### Invoice lifecycle
|
|
102
|
+
|
|
103
|
+
1. **Fulfillment created** — the `order.fulfillment_created` subscriber fires `createInvoiceWorkflow`, which:
|
|
104
|
+
- Creates a **debit** invoice record with an auto-incremented `display_id`
|
|
105
|
+
- Links it to the order via the `invoice_order` link table
|
|
106
|
+
- Generates the PDF and uploads it to the **private** storage bucket
|
|
107
|
+
- Stores the file ID in `invoice.pdf_url`
|
|
108
|
+
2. **Refund processed** — `createCreditInvoiceWorkflow` follows the same steps for a **credit** invoice, referencing the original debit invoice as its parent.
|
|
109
|
+
3. **Download requested** — the API route resolves `invoice.pdf_url` to a presigned download URL via `fileModuleService.retrieveFile()` and redirects the client. Invoices without a stored PDF (created before this feature) are generated on-demand as a fallback.
|
|
110
|
+
|
|
111
|
+
### Invoice numbering
|
|
112
|
+
|
|
113
|
+
`display_id` is a PostgreSQL auto-increment sequence. Voided invoices (created by workflow compensation on failure) preserve their sequence number to avoid gaps.
|
|
114
|
+
|
|
115
|
+
### Localization
|
|
116
|
+
|
|
117
|
+
The PDF language is determined by `order.metadata.locale`. The value is validated against the list of supported locales; if it is absent or unrecognised, `defaultLocale` from the plugin config is used.
|
|
118
|
+
|
|
119
|
+
Supported locales:
|
|
120
|
+
|
|
121
|
+
| Value | Language |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `nl` | Dutch |
|
|
124
|
+
| `nl-be` | Dutch (Belgium) — uses Dutch translations |
|
|
125
|
+
| `en` | English |
|
|
126
|
+
| `de` | German |
|
|
127
|
+
| `fr` | French |
|
|
128
|
+
| `it` | Italian |
|
|
129
|
+
|
|
130
|
+
Set the locale on an order at creation time:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await orderModuleService.updateOrders(orderId, {
|
|
134
|
+
metadata: { locale: "en" },
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API routes
|
|
139
|
+
|
|
140
|
+
### Admin
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
GET /admin/orders/:id/invoice/:invoice_id
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Requires admin authentication. Redirects to a presigned download URL for the invoice PDF.
|
|
147
|
+
|
|
148
|
+
### Store
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
GET /store/orders/:id/invoice
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Requires customer authentication. Returns the debit invoice PDF for the order. The order must belong to the authenticated customer.
|
|
155
|
+
|
|
156
|
+
## Workflows
|
|
157
|
+
|
|
158
|
+
The workflows can be called directly from your own subscribers, jobs, or API routes.
|
|
159
|
+
|
|
160
|
+
### `createInvoiceWorkflow`
|
|
161
|
+
|
|
162
|
+
Creates a debit invoice, links it to an order, and generates + uploads the PDF.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { createInvoiceWorkflow } from "@webbers/invoices-medusa/workflows"
|
|
166
|
+
|
|
167
|
+
await createInvoiceWorkflow(container).run({
|
|
168
|
+
input: { order_id: "order_01J..." },
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `createCreditInvoiceWorkflow`
|
|
173
|
+
|
|
174
|
+
Creates a credit invoice for a refund.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { createCreditInvoiceWorkflow } from "@webbers/invoices-medusa/workflows"
|
|
178
|
+
|
|
179
|
+
await createCreditInvoiceWorkflow(container).run({
|
|
180
|
+
input: {
|
|
181
|
+
order_id: "order_01J...",
|
|
182
|
+
resource_id: "refund_01J...", // refund ID used as resource_id
|
|
183
|
+
parent_invoice_id: "inv_01J...", // optional: the debit invoice this offsets
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `generateInvoicePdfWorkflow`
|
|
189
|
+
|
|
190
|
+
Generates an invoice PDF on-demand and returns it as a base64 string. Useful for attaching to notification emails.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { generateInvoicePdfWorkflow } from "@webbers/invoices-medusa/workflows"
|
|
194
|
+
|
|
195
|
+
const { result } = await generateInvoicePdfWorkflow(container).run({
|
|
196
|
+
input: {
|
|
197
|
+
order_id: "order_01J...",
|
|
198
|
+
invoice_id: "inv_01J...", // optional; omit to generate the debit invoice
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// result.fileName — e.g. "invoice-42.pdf"
|
|
203
|
+
// result.data — base64-encoded PDF
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Backfill script
|
|
207
|
+
|
|
208
|
+
To generate and upload PDFs for invoices created before file storage was introduced:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Dry run (default) — shows what would be processed
|
|
212
|
+
pnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts
|
|
213
|
+
|
|
214
|
+
# Execute
|
|
215
|
+
pnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts -- dry_run=false
|
|
216
|
+
|
|
217
|
+
# Smaller batches if memory is a concern
|
|
218
|
+
pnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts -- dry_run=false batch_size=10
|
|
219
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webbers/invoices-medusa",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Add invoices to Medusa v2",
|
|
5
|
+
"author": "Webbers B.V. <development@webbers.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/webbersagency/invoices-medusa"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
".medusa/server"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./workflows": "./.medusa/server/src/workflows/index.js",
|
|
17
|
+
"./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
|
|
18
|
+
"./modules/*": "./.medusa/server/src/modules/*/index.js",
|
|
19
|
+
"./modules/invoice/service": "./.medusa/server/src/modules/invoice/service.js",
|
|
20
|
+
"./providers/*": "./.medusa/server/src/providers/*/index.js",
|
|
21
|
+
"./types/*": "./.medusa/server/src/types/*/index.js",
|
|
22
|
+
"./links": "./.medusa/server/src/links/index.js",
|
|
23
|
+
"./admin": {
|
|
24
|
+
"import": "./.medusa/server/src/admin/index.mjs",
|
|
25
|
+
"require": "./.medusa/server/src/admin/index.js",
|
|
26
|
+
"default": "./.medusa/server/src/admin/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./*": "./.medusa/server/src/*.js"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"medusa-v2",
|
|
32
|
+
"medusa-plugin-integration",
|
|
33
|
+
"invoices",
|
|
34
|
+
"medusa-webbers"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "medusa plugin:build",
|
|
38
|
+
"dev": "medusa plugin:develop",
|
|
39
|
+
"prepublishOnly": "medusa plugin:build"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@medusajs/admin-sdk": "^2.13.5",
|
|
43
|
+
"@medusajs/cli": "^2.13.5",
|
|
44
|
+
"@medusajs/framework": "^2.13.5",
|
|
45
|
+
"@medusajs/icons": "^2.13.5",
|
|
46
|
+
"@medusajs/medusa": "^2.13.5",
|
|
47
|
+
"@medusajs/test-utils": "^2.13.5",
|
|
48
|
+
"@medusajs/types": "^2.13.5",
|
|
49
|
+
"@medusajs/ui": "4.0.25",
|
|
50
|
+
"@swc/core": "^1.15.21",
|
|
51
|
+
"@types/node": "^20.19.37",
|
|
52
|
+
"@types/pdfmake": "^0.3.2",
|
|
53
|
+
"@types/react": "^18.3.2",
|
|
54
|
+
"@types/react-dom": "^18.2.25",
|
|
55
|
+
"prop-types": "^15.8.1",
|
|
56
|
+
"react": "^18.2.0",
|
|
57
|
+
"react-dom": "^18.2.0",
|
|
58
|
+
"ts-node": "^10.9.2",
|
|
59
|
+
"typescript": "^5.6.2",
|
|
60
|
+
"vite": "^5.2.11",
|
|
61
|
+
"yalc": "^1.0.0-pre.53"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@medusajs/admin-sdk": "^2.13.3",
|
|
65
|
+
"@medusajs/cli": "^2.13.3",
|
|
66
|
+
"@medusajs/framework": "^2.13.3",
|
|
67
|
+
"@medusajs/icons": "^2.13.3",
|
|
68
|
+
"@medusajs/medusa": "^2.13.3",
|
|
69
|
+
"@medusajs/test-utils": "^2.13.3",
|
|
70
|
+
"@medusajs/ui": "4.0.25"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=20"
|
|
74
|
+
},
|
|
75
|
+
"packageManager": "pnpm@10.33.0",
|
|
76
|
+
"dependencies": {
|
|
77
|
+
"@medusajs/admin-shared": "^2.13.5",
|
|
78
|
+
"@medusajs/dashboard": "^2.13.5",
|
|
79
|
+
"@medusajs/js-sdk": "^2.13.5",
|
|
80
|
+
"@tanstack/react-query": "5.64.2",
|
|
81
|
+
"date-fns": "^4.1.0",
|
|
82
|
+
"lodash.merge": "^4.6.2",
|
|
83
|
+
"pdfmake": "^0.3.7",
|
|
84
|
+
"react-i18next": "^15.7.4"
|
|
85
|
+
}
|
|
86
|
+
}
|