@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.
Files changed (35) hide show
  1. package/.medusa/server/src/admin/index.js +86 -0
  2. package/.medusa/server/src/admin/index.mjs +85 -0
  3. package/.medusa/server/src/api/admin/orders/[id]/invoice/[invoice_id]/route.js +28 -0
  4. package/.medusa/server/src/api/middlewares.js +13 -0
  5. package/.medusa/server/src/api/store/orders/[id]/invoice/route.js +37 -0
  6. package/.medusa/server/src/core/classes/pdf-generator/index.js +36 -0
  7. package/.medusa/server/src/core/classes/pdf-generator/templates/base.js +32 -0
  8. package/.medusa/server/src/core/classes/pdf-generator/templates/credit-invoice-content.js +308 -0
  9. package/.medusa/server/src/core/classes/pdf-generator/templates/invoice-content.js +496 -0
  10. package/.medusa/server/src/core/classes/pdf-generator/templates/packing-slip-content.js +164 -0
  11. package/.medusa/server/src/i18n/index.js +41 -0
  12. package/.medusa/server/src/i18n/messages/de.js +80 -0
  13. package/.medusa/server/src/i18n/messages/en.js +80 -0
  14. package/.medusa/server/src/i18n/messages/fr.js +80 -0
  15. package/.medusa/server/src/i18n/messages/it.js +80 -0
  16. package/.medusa/server/src/i18n/messages/nl.js +80 -0
  17. package/.medusa/server/src/links/invoice-order.js +17 -0
  18. package/.medusa/server/src/modules/invoice/index.js +15 -0
  19. package/.medusa/server/src/modules/invoice/loaders/validate.js +12 -0
  20. package/.medusa/server/src/modules/invoice/migrations/Migration20260312154721.js +18 -0
  21. package/.medusa/server/src/modules/invoice/migrations/Migration20260403000001.js +16 -0
  22. package/.medusa/server/src/modules/invoice/models/invoice.js +14 -0
  23. package/.medusa/server/src/modules/invoice/service.js +45 -0
  24. package/.medusa/server/src/subscribers/fulfillment-created-invoice.js +39 -0
  25. package/.medusa/server/src/subscribers/payment-refunded-invoice.js +65 -0
  26. package/.medusa/server/src/types/index.js +3 -0
  27. package/.medusa/server/src/utils/format-locale-amount.js +12 -0
  28. package/.medusa/server/src/workflows/create-credit-invoice.js +37 -0
  29. package/.medusa/server/src/workflows/create-invoice.js +38 -0
  30. package/.medusa/server/src/workflows/generate-invoice-pdf.js +10 -0
  31. package/.medusa/server/src/workflows/steps/create-invoice-step.js +25 -0
  32. package/.medusa/server/src/workflows/steps/generate-invoice-pdf-step.js +61 -0
  33. package/.medusa/server/src/workflows/steps/upload-invoice-pdf-step.js +29 -0
  34. package/README.md +219 -0
  35. 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
+ }