@nuxt-customer-portal/invoices 0.3.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 (82) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +4 -0
  4. package/app/assets/invoices.css +332 -0
  5. package/app/components/InvoicesAdminEmptyState.vue +28 -0
  6. package/app/components/InvoicesAdminInvoices.vue +950 -0
  7. package/app/components/InvoicesAdminListToolbar.vue +19 -0
  8. package/app/components/InvoicesAdminPaginatedList.vue +95 -0
  9. package/app/components/InvoicesClientAccessPanel.vue +63 -0
  10. package/app/components/InvoicesClientSettingsPanel.vue +150 -0
  11. package/app/components/InvoicesDashboardCard.vue +29 -0
  12. package/app/components/InvoicesDashboardReceivedInvoices.vue +1 -0
  13. package/app/components/InvoicesDashboardSalesInvoices.vue +1 -0
  14. package/app/components/InvoicesInvoiceDetail.vue +1109 -0
  15. package/app/components/InvoicesInvoiceEmailModal.vue +223 -0
  16. package/app/components/InvoicesPageShell.vue +7 -0
  17. package/app/components/InvoicesSenderDetailsForm.vue +229 -0
  18. package/app/composables/useInvoiceSources.ts +9 -0
  19. package/app/composables/useInvoices.ts +68 -0
  20. package/app/composables/useInvoicesAdminList.ts +113 -0
  21. package/app/pages/admin/invoices/[id].vue +36 -0
  22. package/app/pages/admin/invoices/index.vue +61 -0
  23. package/app/pages/admin/invoices/new.vue +18 -0
  24. package/app/pages/admin/invoices/settings.vue +86 -0
  25. package/app/pages/invoices/[id].vue +37 -0
  26. package/app/pages/invoices/index.vue +131 -0
  27. package/app/pages/invoices/viewers.vue +115 -0
  28. package/app/plugins/invoices-feature.ts +60 -0
  29. package/i18n/locales/en.json +306 -0
  30. package/i18n/locales/nl.json +306 -0
  31. package/migrations/0000_baseline.sql +235 -0
  32. package/migrations/meta/0000_snapshot.json +1318 -0
  33. package/migrations/meta/_journal.json +13 -0
  34. package/nuxt.config.ts +15 -0
  35. package/package.json +60 -0
  36. package/portal.manifest.mjs +9 -0
  37. package/server/api/invoices/admin/bootstrap.get.ts +17 -0
  38. package/server/api/invoices/admin/clients/[clientId]/access.get.ts +11 -0
  39. package/server/api/invoices/admin/clients/[clientId]/contacts/[id].delete.ts +8 -0
  40. package/server/api/invoices/admin/clients/[clientId]/contacts/[id].patch.ts +13 -0
  41. package/server/api/invoices/admin/clients/[clientId]/contacts/index.get.ts +8 -0
  42. package/server/api/invoices/admin/clients/[clientId]/contacts/index.post.ts +12 -0
  43. package/server/api/invoices/admin/email-domain.get.ts +17 -0
  44. package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].delete.ts +23 -0
  45. package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].get.ts +30 -0
  46. package/server/api/invoices/admin/invoices/[id]/attachments.post.ts +27 -0
  47. package/server/api/invoices/admin/invoices/[id]/email-preview.get.ts +22 -0
  48. package/server/api/invoices/admin/invoices/[id]/email-status.post.ts +18 -0
  49. package/server/api/invoices/admin/invoices/[id]/email.post.ts +24 -0
  50. package/server/api/invoices/admin/invoices/[id]/index.get.ts +17 -0
  51. package/server/api/invoices/admin/invoices/[id]/issue.post.ts +24 -0
  52. package/server/api/invoices/admin/invoices/[id]/payments.post.ts +24 -0
  53. package/server/api/invoices/admin/invoices/[id]/pdf.get.ts +29 -0
  54. package/server/api/invoices/admin/invoices/[id]/reminder-preview.get.ts +24 -0
  55. package/server/api/invoices/admin/invoices/[id]/reminder.post.ts +25 -0
  56. package/server/api/invoices/admin/invoices/[id].patch.ts +24 -0
  57. package/server/api/invoices/admin/invoices/index.get.ts +20 -0
  58. package/server/api/invoices/admin/invoices/index.post.ts +19 -0
  59. package/server/api/invoices/admin/invoices/next-number.get.ts +16 -0
  60. package/server/api/invoices/admin/settings.put.ts +9 -0
  61. package/server/api/invoices/capabilities.get.ts +25 -0
  62. package/server/api/invoices/client/[accessId]/viewers.get.ts +10 -0
  63. package/server/api/invoices/client/[accessId]/viewers.put.ts +17 -0
  64. package/server/api/invoices/client/invoices/[id]/attachments/[attachmentId].get.ts +20 -0
  65. package/server/api/invoices/client/invoices/[id]/index.get.ts +12 -0
  66. package/server/api/invoices/client/invoices/[id]/pdf.get.ts +23 -0
  67. package/server/api/invoices/client/invoices/index.get.ts +14 -0
  68. package/server/api/invoices/client/suppliers.get.ts +8 -0
  69. package/server/db/schema/invoices.ts +251 -0
  70. package/server/plugins/openapi-contracts.ts +55 -0
  71. package/server/utils/invoice-email-status.ts +64 -0
  72. package/server/utils/invoice-email.ts +240 -0
  73. package/server/utils/invoice-listing.ts +71 -0
  74. package/server/utils/invoice-pdf.ts +358 -0
  75. package/server/utils/invoice-repository.ts +821 -0
  76. package/server/utils/invoice-validation.ts +112 -0
  77. package/shared/email-delivery-status.ts +23 -0
  78. package/shared/feature.ts +148 -0
  79. package/shared/invoice-introduction.ts +21 -0
  80. package/shared/invoice-number.ts +24 -0
  81. package/shared/types/invoice.ts +251 -0
  82. package/tokens.css +25 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1786525252052,
9
+ "tag": "0000_baseline",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
package/nuxt.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { fileURLToPath } from 'node:url'
2
+
3
+ export default defineNuxtConfig({
4
+ $meta: { name: 'nuxt-customer-portal-invoices' },
5
+ compatibilityDate: '2025-10-24',
6
+ modules: ['@nuxtjs/i18n'],
7
+ css: [fileURLToPath(new URL('./app/assets/invoices.css', import.meta.url))],
8
+ components: [{ path: fileURLToPath(new URL('./app/components', import.meta.url)), global: true }],
9
+ i18n: {
10
+ locales: [
11
+ { code: 'en', iso: 'en-US', name: 'English', file: 'en.json' },
12
+ { code: 'nl', iso: 'nl-NL', name: 'Nederlands', file: 'nl.json' }
13
+ ]
14
+ }
15
+ })
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@nuxt-customer-portal/invoices",
3
+ "version": "0.3.0",
4
+ "license": "MIT",
5
+ "homepage": "https://nuxt-customer-portal.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ludulicious/nuxt-customer-portal.git"
9
+ },
10
+ "type": "module",
11
+ "main": "./nuxt.config.ts",
12
+ "files": [
13
+ "LICENSE",
14
+ "CHANGELOG.md",
15
+ "README.md",
16
+ "app",
17
+ "i18n",
18
+ "migrations",
19
+ "server",
20
+ "shared",
21
+ "tokens.css",
22
+ "nuxt.config.ts",
23
+ "portal.manifest.mjs"
24
+ ],
25
+ "exports": {
26
+ ".": "./nuxt.config.ts",
27
+ "./feature": "./shared/feature.ts",
28
+ "./types": "./shared/types/invoice.ts",
29
+ "./schema": "./server/db/schema/invoices.ts",
30
+ "./portal-manifest": "./portal.manifest.mjs",
31
+ "./app/*": "./app/*.ts",
32
+ "./server/*": "./server/*.ts",
33
+ "./shared/*": "./shared/*.ts"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@nuxtjs/i18n": "^10.6.0",
40
+ "@types/pg": "8.15.5",
41
+ "drizzle-orm": "^0.45.2",
42
+ "nanoid": "^5.1.6",
43
+ "pdf-lib": "^1.17.1",
44
+ "pg": "^8.22.0",
45
+ "resend": "^6.18.1",
46
+ "zod": "^4.4.3",
47
+ "@nuxt-customer-portal/core": "^0.3.0",
48
+ "@nuxt-customer-portal/ui": "^0.3.0",
49
+ "@nuxt-customer-portal/organizations": "^0.3.0",
50
+ "@nuxt-customer-portal/clients": "^0.3.0"
51
+ },
52
+ "peerDependencies": {
53
+ "nuxt": "^4.5.1",
54
+ "vue": "^3.5.0"
55
+ },
56
+ "devDependencies": {
57
+ "nuxt": "^4.5.1",
58
+ "vue": "^3.5.0"
59
+ }
60
+ }
@@ -0,0 +1,9 @@
1
+ export default {
2
+ id: 'invoices',
3
+ version: '0.3.0',
4
+ source: '@nuxt-customer-portal/invoices',
5
+ clientModuleId: 'invoices',
6
+ dependsOn: ['core', 'ui', 'organizations', 'clients'],
7
+ schema: './server/db/schema/invoices.ts',
8
+ migrations: './migrations'
9
+ }
@@ -0,0 +1,17 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import {
4
+ getInvoiceSettings,
5
+ getOrganizationInvoiceProfile,
6
+ listInvoiceClients
7
+ } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
8
+
9
+ export default defineEventHandler(async (event) => {
10
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
11
+ const [settings, clients, organizationProfile] = await Promise.all([
12
+ getInvoiceSettings(organizationId),
13
+ listInvoiceClients(organizationId),
14
+ getOrganizationInvoiceProfile(organizationId)
15
+ ])
16
+ return { settings, clients, organizationProfile }
17
+ })
@@ -0,0 +1,11 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { getClientInvoiceAccessOverview } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const { organizationId, organizationType } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
7
+ if (organizationType !== 'PROVIDER') {
8
+ throw createError({ statusCode: 403, message: 'Access denied' })
9
+ }
10
+ return getClientInvoiceAccessOverview(organizationId, getRouterParam(event, 'clientId')!)
11
+ })
@@ -0,0 +1,8 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { deleteBillingContact } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
7
+ return deleteBillingContact(getRouterParam(event, 'clientId')!, getRouterParam(event, 'id')!)
8
+ })
@@ -0,0 +1,13 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { updateBillingContact } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+ import { billingContactUpdateSchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
8
+ return updateBillingContact(
9
+ getRouterParam(event, 'clientId')!,
10
+ getRouterParam(event, 'id')!,
11
+ billingContactUpdateSchema.parse(await readBody(event))
12
+ )
13
+ })
@@ -0,0 +1,8 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { listBillingContacts } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
7
+ return listBillingContacts(getRouterParam(event, 'clientId')!)
8
+ })
@@ -0,0 +1,12 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { createBillingContact } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+ import { billingContactCreateSchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
8
+ return createBillingContact(
9
+ getRouterParam(event, 'clientId')!,
10
+ billingContactCreateSchema.parse(await readBody(event))
11
+ )
12
+ })
@@ -0,0 +1,17 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { getPortalEmailProviderStatus } from '@nuxt-customer-portal/core/server/utils/portal-email'
3
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
4
+ import { getOrganizationInvoiceProfile } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
8
+ const profile = await getOrganizationInvoiceProfile(organizationId)
9
+ const domain = profile.invoiceEmail?.split('@')[1]?.toLowerCase() ?? null
10
+ const status = await getPortalEmailProviderStatus()
11
+ return {
12
+ email: profile.invoiceEmail,
13
+ domain,
14
+ configured: status.configured,
15
+ verified: Boolean(domain && status.verifiedDomains.includes(domain))
16
+ }
17
+ })
@@ -0,0 +1,23 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { deleteInvoiceAttachment } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesByIdAttachmentsByAttachmentIdDelete',
9
+ summary: 'Delete an invoice attachment',
10
+ description:
11
+ 'Delete an invoice attachment. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ return deleteInvoiceAttachment(
18
+ organizationId,
19
+ session.user.id,
20
+ getRouterParam(event, 'id')!,
21
+ getRouterParam(event, 'attachmentId')!
22
+ )
23
+ })
@@ -0,0 +1,30 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { getInvoiceAttachment } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesByIdAttachmentsByAttachmentIdGet',
9
+ summary: 'Download an invoice attachment',
10
+ description:
11
+ 'Download an invoice attachment. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ const attachment = await getInvoiceAttachment(
18
+ organizationId,
19
+ getRouterParam(event, 'id')!,
20
+ getRouterParam(event, 'attachmentId')!
21
+ )
22
+ setResponseHeader(event, 'content-type', attachment.contentType)
23
+ setResponseHeader(event, 'content-length', attachment.size)
24
+ setResponseHeader(
25
+ event,
26
+ 'content-disposition',
27
+ `attachment; filename*=UTF-8''${encodeURIComponent(attachment.fileName)}`
28
+ )
29
+ return Buffer.from(attachment.contentBase64, 'base64')
30
+ })
@@ -0,0 +1,27 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { addInvoiceAttachment } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesByIdAttachmentsPost',
9
+ summary: 'Upload an invoice attachment',
10
+ description:
11
+ 'Upload an invoice attachment. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ const parts = await readMultipartFormData(event)
18
+ const file = parts?.find((part) => part.name === 'file' && part.filename)
19
+ if (!file?.filename) {
20
+ throw createError({ statusCode: 400, message: 'Select a file to attach' })
21
+ }
22
+ return addInvoiceAttachment(organizationId, session.user.id, getRouterParam(event, 'id')!, {
23
+ fileName: file.filename,
24
+ contentType: file.type || 'application/octet-stream',
25
+ data: file.data
26
+ })
27
+ })
@@ -0,0 +1,22 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { getInvoiceEmailPreview } from '@nuxt-customer-portal/invoices/server/utils/invoice-email'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesByIdEmailPreviewGet',
9
+ summary: 'Preview an invoice email',
10
+ description: 'Preview an invoice email. Scoped to the active organization and the applicable Invoices permission.'
11
+ }
12
+ })
13
+
14
+ export default defineEventHandler(async (event) => {
15
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
16
+ const locale = getQuery(event).locale
17
+ return getInvoiceEmailPreview(
18
+ organizationId,
19
+ getRouterParam(event, 'id')!,
20
+ typeof locale === 'string' ? locale : undefined
21
+ )
22
+ })
@@ -0,0 +1,18 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { refreshInvoiceEmailStatuses } from '@nuxt-customer-portal/invoices/server/utils/invoice-email-status'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesByIdEmailStatusPost',
9
+ summary: 'Refresh invoice email delivery status',
10
+ description:
11
+ 'Refresh invoice email delivery status. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ return refreshInvoiceEmailStatuses(organizationId, getRouterParam(event, 'id')!, getQuery(event).refresh === '1')
18
+ })
@@ -0,0 +1,24 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { deliverInvoiceEmail } from '@nuxt-customer-portal/invoices/server/utils/invoice-email'
4
+ import { invoiceEmailDeliverySchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Invoices'],
9
+ operationId: 'invoicesAdminInvoicesByIdEmailPost',
10
+ summary: 'Send an invoice email',
11
+ description: 'Send an invoice email. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ return deliverInvoiceEmail(
18
+ organizationId,
19
+ session.user.id,
20
+ getRouterParam(event, 'id')!,
21
+ invoiceEmailDeliverySchema.parse(await readBody(event)),
22
+ false
23
+ )
24
+ })
@@ -0,0 +1,17 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { getInvoice } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesByIdGet',
9
+ summary: 'Get an invoice',
10
+ description: 'Get an invoice. Scoped to the active organization and the applicable Invoices permission.'
11
+ }
12
+ })
13
+
14
+ export default defineEventHandler(async (event) => {
15
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
16
+ return getInvoice(organizationId, getRouterParam(event, 'id')!)
17
+ })
@@ -0,0 +1,24 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { deliverInvoiceEmail } from '@nuxt-customer-portal/invoices/server/utils/invoice-email'
4
+ import { invoiceEmailDeliverySchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Invoices'],
9
+ operationId: 'invoicesAdminInvoicesByIdIssuePost',
10
+ summary: 'Issue an invoice',
11
+ description: 'Issue an invoice. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ return deliverInvoiceEmail(
18
+ organizationId,
19
+ session.user.id,
20
+ getRouterParam(event, 'id')!,
21
+ invoiceEmailDeliverySchema.parse(await readBody(event)),
22
+ true
23
+ )
24
+ })
@@ -0,0 +1,24 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { registerInvoicePayment } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+ import { invoicePaymentSchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Invoices'],
9
+ operationId: 'invoicesAdminInvoicesByIdPaymentsPost',
10
+ summary: 'Register an invoice payment',
11
+ description:
12
+ 'Register an invoice payment. Scoped to the active organization and the applicable Invoices permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
18
+ return registerInvoicePayment(
19
+ organizationId,
20
+ session.user.id,
21
+ getRouterParam(event, 'id')!,
22
+ invoicePaymentSchema.parse(await readBody(event))
23
+ )
24
+ })
@@ -0,0 +1,29 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { generateInvoicePdf } from '@nuxt-customer-portal/invoices/server/utils/invoice-pdf'
4
+ import { getInvoice } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Invoices'],
9
+ operationId: 'invoicesAdminInvoicesByIdPdfGet',
10
+ summary: 'Download an invoice PDF',
11
+ description: 'Download an invoice PDF. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ const invoice = await getInvoice(organizationId, getRouterParam(event, 'id')!)
18
+ const locale = getQuery(event).locale
19
+ const disposition = getQuery(event).download === '1' ? 'attachment' : 'inline'
20
+ const pdf = await generateInvoicePdf(invoice, typeof locale === 'string' ? locale : undefined)
21
+ const fileName = `invoice-${invoice.number.replace(/[^a-z0-9._-]+/gi, '-')}.pdf`
22
+
23
+ setResponseHeaders(event, {
24
+ 'Content-Type': 'application/pdf',
25
+ 'Content-Disposition': `${disposition}; filename="${fileName}"`,
26
+ 'Cache-Control': 'private, no-store'
27
+ })
28
+ return Buffer.from(pdf)
29
+ })
@@ -0,0 +1,24 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { getInvoiceEmailPreview } from '@nuxt-customer-portal/invoices/server/utils/invoice-email'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesByIdReminderPreviewGet',
9
+ summary: 'Preview an invoice reminder',
10
+ description:
11
+ 'Preview an invoice reminder. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ const locale = getQuery(event).locale
18
+ return getInvoiceEmailPreview(
19
+ organizationId,
20
+ getRouterParam(event, 'id')!,
21
+ typeof locale === 'string' ? locale : undefined,
22
+ 'REMINDER'
23
+ )
24
+ })
@@ -0,0 +1,25 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { deliverInvoiceEmail } from '@nuxt-customer-portal/invoices/server/utils/invoice-email'
4
+ import { invoiceEmailDeliverySchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Invoices'],
9
+ operationId: 'invoicesAdminInvoicesByIdReminderPost',
10
+ summary: 'Send an invoice reminder',
11
+ description: 'Send an invoice reminder. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ return deliverInvoiceEmail(
18
+ organizationId,
19
+ session.user.id,
20
+ getRouterParam(event, 'id')!,
21
+ invoiceEmailDeliverySchema.parse(await readBody(event)),
22
+ false,
23
+ 'REMINDER'
24
+ )
25
+ })
@@ -0,0 +1,24 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { changeInvoiceStatus, updateInvoice } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+ import { invoiceIssueSchema, invoiceUpdateSchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Invoices'],
9
+ operationId: 'invoicesAdminInvoicesByIdPatch',
10
+ summary: 'Update an invoice',
11
+ description: 'Update an invoice. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ const body = await readBody(event)
18
+ const id = getRouterParam(event, 'id')!
19
+ if ('action' in body) {
20
+ const action = invoiceIssueSchema.parse(body).action
21
+ return changeInvoiceStatus(organizationId, session.user.id, id, action)
22
+ }
23
+ return updateInvoice(organizationId, session.user.id, id, invoiceUpdateSchema.parse(body))
24
+ })
@@ -0,0 +1,20 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { invoiceListQuerySchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
4
+ import { listInvoicesPage } from '@nuxt-customer-portal/invoices/server/utils/invoice-listing'
5
+ import { requireInvoicesEnabled } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
6
+
7
+ defineRouteMeta({
8
+ openAPI: {
9
+ tags: ['Invoices'],
10
+ operationId: 'invoicesAdminInvoicesGet',
11
+ summary: 'List invoices',
12
+ description: 'List invoices. Scoped to the active organization and the applicable Invoices permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
18
+ await requireInvoicesEnabled(organizationId)
19
+ return listInvoicesPage(organizationId, invoiceListQuerySchema.parse(getQuery(event)))
20
+ })
@@ -0,0 +1,19 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { createInvoice, requireInvoicesEnabled } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+ import { invoiceCreateSchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Invoices'],
9
+ operationId: 'invoicesAdminInvoicesPost',
10
+ summary: 'Create an invoice',
11
+ description: 'Create an invoice. Scoped to the active organization and the applicable Invoices permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
17
+ await requireInvoicesEnabled(organizationId)
18
+ return createInvoice(organizationId, session.user.id, invoiceCreateSchema.parse(await readBody(event)))
19
+ })
@@ -0,0 +1,16 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { getNextInvoiceNumber } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Invoices'],
8
+ operationId: 'invoicesAdminInvoicesNextNumberGet',
9
+ summary: 'Suggest the next invoice number'
10
+ }
11
+ })
12
+
13
+ export default defineEventHandler(async (event) => {
14
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
15
+ return { number: await getNextInvoiceNumber(organizationId) }
16
+ })
@@ -0,0 +1,9 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { invoicesFeature } from '@nuxt-customer-portal/invoices/shared/feature'
3
+ import { updateInvoiceSettings } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
4
+ import { invoiceSettingsSchema } from '@nuxt-customer-portal/invoices/server/utils/invoice-validation'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ const { organizationId } = await requireFeatureAccess(event, invoicesFeature.policy, 'manage')
8
+ return updateInvoiceSettings(organizationId, invoiceSettingsSchema.parse(await readBody(event)))
9
+ })
@@ -0,0 +1,25 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { db, requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
3
+ import { invoiceSettings } from '@nuxt-customer-portal/invoices/server/db/schema/invoices'
4
+ import { listClientInvoiceSuppliers } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ const { session, organizationId, organizationType, role } = await requireActiveOrganizationRole(event)
8
+ const isAdmin = role === 'owner' || role === 'admin' || session.user.role === 'admin'
9
+ const settings = await db
10
+ .select({ enabled: invoiceSettings.enabled })
11
+ .from(invoiceSettings)
12
+ .where(eq(invoiceSettings.organizationId, organizationId))
13
+ .limit(1)
14
+ const suppliers =
15
+ organizationType === 'CLIENT' ? await listClientInvoiceSuppliers(organizationId, session.user.id, isAdmin) : []
16
+ const invoicesEnabled = settings[0]?.enabled ?? false
17
+ const canConfigureInvoices = organizationType === 'PROVIDER' && isAdmin
18
+ return {
19
+ canConfigureInvoices,
20
+ canManageInvoices: canConfigureInvoices && invoicesEnabled,
21
+ canViewReceivedInvoices: suppliers.length > 0,
22
+ canManageViewers: isAdmin && suppliers.length > 0,
23
+ invoicesEnabled
24
+ }
25
+ })
@@ -0,0 +1,10 @@
1
+ import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
2
+ import { listClientInvoiceViewers } from '@nuxt-customer-portal/invoices/server/utils/invoice-repository'
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const { organizationId, role } = await requireActiveOrganizationRole(event)
6
+ if (!['owner', 'admin'].includes(role)) {
7
+ throw createError({ statusCode: 403, message: 'Client administrator access required' })
8
+ }
9
+ return listClientInvoiceViewers(getRouterParam(event, 'accessId')!, organizationId)
10
+ })