@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +4 -0
- package/app/assets/invoices.css +332 -0
- package/app/components/InvoicesAdminEmptyState.vue +28 -0
- package/app/components/InvoicesAdminInvoices.vue +950 -0
- package/app/components/InvoicesAdminListToolbar.vue +19 -0
- package/app/components/InvoicesAdminPaginatedList.vue +95 -0
- package/app/components/InvoicesClientAccessPanel.vue +63 -0
- package/app/components/InvoicesClientSettingsPanel.vue +150 -0
- package/app/components/InvoicesDashboardCard.vue +29 -0
- package/app/components/InvoicesDashboardReceivedInvoices.vue +1 -0
- package/app/components/InvoicesDashboardSalesInvoices.vue +1 -0
- package/app/components/InvoicesInvoiceDetail.vue +1109 -0
- package/app/components/InvoicesInvoiceEmailModal.vue +223 -0
- package/app/components/InvoicesPageShell.vue +7 -0
- package/app/components/InvoicesSenderDetailsForm.vue +229 -0
- package/app/composables/useInvoiceSources.ts +9 -0
- package/app/composables/useInvoices.ts +68 -0
- package/app/composables/useInvoicesAdminList.ts +113 -0
- package/app/pages/admin/invoices/[id].vue +36 -0
- package/app/pages/admin/invoices/index.vue +61 -0
- package/app/pages/admin/invoices/new.vue +18 -0
- package/app/pages/admin/invoices/settings.vue +86 -0
- package/app/pages/invoices/[id].vue +37 -0
- package/app/pages/invoices/index.vue +131 -0
- package/app/pages/invoices/viewers.vue +115 -0
- package/app/plugins/invoices-feature.ts +60 -0
- package/i18n/locales/en.json +306 -0
- package/i18n/locales/nl.json +306 -0
- package/migrations/0000_baseline.sql +235 -0
- package/migrations/meta/0000_snapshot.json +1318 -0
- package/migrations/meta/_journal.json +13 -0
- package/nuxt.config.ts +15 -0
- package/package.json +60 -0
- package/portal.manifest.mjs +9 -0
- package/server/api/invoices/admin/bootstrap.get.ts +17 -0
- package/server/api/invoices/admin/clients/[clientId]/access.get.ts +11 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/[id].delete.ts +8 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/[id].patch.ts +13 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/index.get.ts +8 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/index.post.ts +12 -0
- package/server/api/invoices/admin/email-domain.get.ts +17 -0
- package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].delete.ts +23 -0
- package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].get.ts +30 -0
- package/server/api/invoices/admin/invoices/[id]/attachments.post.ts +27 -0
- package/server/api/invoices/admin/invoices/[id]/email-preview.get.ts +22 -0
- package/server/api/invoices/admin/invoices/[id]/email-status.post.ts +18 -0
- package/server/api/invoices/admin/invoices/[id]/email.post.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/index.get.ts +17 -0
- package/server/api/invoices/admin/invoices/[id]/issue.post.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/payments.post.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/pdf.get.ts +29 -0
- package/server/api/invoices/admin/invoices/[id]/reminder-preview.get.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/reminder.post.ts +25 -0
- package/server/api/invoices/admin/invoices/[id].patch.ts +24 -0
- package/server/api/invoices/admin/invoices/index.get.ts +20 -0
- package/server/api/invoices/admin/invoices/index.post.ts +19 -0
- package/server/api/invoices/admin/invoices/next-number.get.ts +16 -0
- package/server/api/invoices/admin/settings.put.ts +9 -0
- package/server/api/invoices/capabilities.get.ts +25 -0
- package/server/api/invoices/client/[accessId]/viewers.get.ts +10 -0
- package/server/api/invoices/client/[accessId]/viewers.put.ts +17 -0
- package/server/api/invoices/client/invoices/[id]/attachments/[attachmentId].get.ts +20 -0
- package/server/api/invoices/client/invoices/[id]/index.get.ts +12 -0
- package/server/api/invoices/client/invoices/[id]/pdf.get.ts +23 -0
- package/server/api/invoices/client/invoices/index.get.ts +14 -0
- package/server/api/invoices/client/suppliers.get.ts +8 -0
- package/server/db/schema/invoices.ts +251 -0
- package/server/plugins/openapi-contracts.ts +55 -0
- package/server/utils/invoice-email-status.ts +64 -0
- package/server/utils/invoice-email.ts +240 -0
- package/server/utils/invoice-listing.ts +71 -0
- package/server/utils/invoice-pdf.ts +358 -0
- package/server/utils/invoice-repository.ts +821 -0
- package/server/utils/invoice-validation.ts +112 -0
- package/shared/email-delivery-status.ts +23 -0
- package/shared/feature.ts +148 -0
- package/shared/invoice-introduction.ts +21 -0
- package/shared/invoice-number.ts +24 -0
- package/shared/types/invoice.ts +251 -0
- package/tokens.css +25 -0
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,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
|
+
})
|