Compare commits

..

1 Commits

Author SHA1 Message Date
Constantin Graf
b22132b0f2 Add permissions and types for invoice recipients 2026-07-29 16:45:49 +02:00
7 changed files with 188 additions and 200 deletions

View File

@@ -1,149 +0,0 @@
name: Playwright Tests - On Premise
on:
push:
branches:
- main
- develop
- feature/support_extension_e2e_tests
tags:
- '*'
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
services:
mailpit:
image: 'axllent/mailpit:latest'
ports:
- 1025:1025
- 8025:8025
pgsql_test:
image: postgres:15
env:
PGPASSWORD: 'root'
POSTGRES_DB: 'laravel'
POSTGRES_USER: 'root'
POSTGRES_PASSWORD: 'root'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
# Renders the invoice PDFs.
gotenberg:
image: gotenberg/gotenberg:8
ports:
- 3000:3000
options: >-
--health-cmd "curl --silent --fail http://localhost:3000/health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: "Checkout code"
uses: actions/checkout@v7
- name: "Setup node"
uses: actions/setup-node@v7
with:
node-version: '20.x'
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: none
- name: "Run composer install"
run: composer install -n --prefer-dist
- name: "Read extension manifest"
id: extension-manifest
run: |
{
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT"
- name: "Checkout invoicing extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
path: extensions/Invoicing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
- name: "Install composer dependencies in invoicing extension"
run: cd extensions/Invoicing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
- name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci
- name: "Prepare Laravel Application"
run: |
cp .env.ci .env
php artisan key:generate
php artisan passport:keys
# Must run before `migrate` so the extension's migrations are applied, and
# before the frontend build so vite collects the extension's assets.
- name: "Activate invoicing extension"
run: php artisan module:enable Invoicing
- name: "Migrate and seed"
run: php artisan migrate --seed
- name: "Install dependencies"
run: npm ci
- name: "Build Frontend"
run: npm run build
- name: "Install FrankenPHP"
run: |
ARCH="$(uname -m)"
curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-${ARCH}" -o /usr/local/bin/frankenphp
chmod +x /usr/local/bin/frankenphp
- name: "Run Laravel Octane Server"
run: php artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000 --workers=4 --max-requests=500 > /dev/null 2>&1 &
env:
OCTANE_SERVER: frankenphp
- name: "Install Playwright Browsers"
run: npx playwright install --with-deps
- name: "Run Playwright tests"
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
MAILPIT_BASE_URL: 'http://localhost:8025'
# No artifacts are uploaded on purpose. This repository is public, so
# every artifact is downloadable by anyone - and Playwright's reports
# carry the source of the specs that produced them: traces embed whole
# source files under resources/src@*, and each error-context.md attachment
# embeds a ~200 line window of the spec plus a page snapshot. Those specs
# live in the private extension repository, so uploading them would
# publish private source on any failing or flaky run.
#
# Failures are diagnosed from the job log, which still shows the error and
# a short code frame. To inspect a trace, reproduce the failure locally
# with the extension checked out.
#
# This is a mitigation, not a fix: the code frame in the log is itself a
# handful of lines of private source. Only running this workflow from a
# private repository removes that exposure.

View File

@@ -80,6 +80,10 @@ class PermissionStore
'invoices:update',
'invoices:download',
'invoices:delete',
'invoice-recipients:view',
'invoice-recipients:create',
'invoice-recipients:update',
'invoice-recipients:delete',
'invoice-settings:view',
'invoice-settings:update',
],
@@ -147,6 +151,10 @@ class PermissionStore
'invoices:update',
'invoices:download',
'invoices:delete',
'invoice-recipients:view',
'invoice-recipients:create',
'invoice-recipients:update',
'invoice-recipients:delete',
'invoice-settings:view',
'invoice-settings:update',
],
@@ -203,6 +211,10 @@ class PermissionStore
'invoices:update',
'invoices:download',
'invoices:delete',
'invoice-recipients:view',
'invoice-recipients:create',
'invoice-recipients:update',
'invoice-recipients:delete',
'invoice-settings:view',
'invoice-settings:update',
],

View File

@@ -1,7 +1,7 @@
{
"Billing": {
"repository": "solidtime-io/extension-billing",
"ref": "v0.0.3"
"ref": "v0.0.4"
},
"Services": {
"repository": "solidtime-io/extension-services",
@@ -9,6 +9,6 @@
},
"Invoicing": {
"repository": "solidtime-io/extension-invoicing",
"ref": "feature/e2e_tests"
"ref": "feature/recipients"
}
}

View File

@@ -10,20 +10,7 @@ import { defineConfig, devices } from '@playwright/test';
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: '.',
testMatch: ['e2e/**/*.spec.ts', 'extensions/*/tests/e2e/**/*.spec.ts'],
testIgnore: [
'**/node_modules/**',
'**/vendor/**',
'.git/**',
'.claude/**',
'public/**',
'storage/**',
'test-results/**',
'playwright-report/**',
'blob-report/**',
],
tsconfig: './tsconfig.json',
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */

View File

@@ -117,6 +117,8 @@ export type DetailedInvoiceResponse = ZodiosResponseByAlias<SolidTimeApi, 'getIn
export type DetailedInvoice = DetailedInvoiceResponse['data'];
export type InvoiceIndexEntry = ZodiosResponseByAlias<SolidTimeApi, 'getInvoices'>['data'][0];
export type InvoiceRecipient = ZodiosResponseByAlias<SolidTimeApi, 'getInvoiceRecipients'>['data'][0];
export type InvoiceRecipientBody = ZodiosBodyByAlias<SolidTimeApi, 'createInvoiceRecipient'>;
export type UpdateInvoiceSettings = ZodiosBodyByAlias<SolidTimeApi, 'updateInvoiceSettings'>;

View File

@@ -45,14 +45,54 @@ const InvitationResource = z
const InvitationStoreRequest = z
.object({ email: z.string().email(), role: z.enum(['admin', 'manager', 'employee']) })
.passthrough();
const InvoiceRecipientResource = z
.object({
id: z.string(),
organization_id: z.string(),
name: z.string(),
vatin: z.union([z.string(), z.null()]),
address_line_1: z.union([z.string(), z.null()]),
address_line_2: z.union([z.string(), z.null()]),
address_line_3: z.union([z.string(), z.null()]),
address_post_code: z.union([z.string(), z.null()]),
address_city: z.union([z.string(), z.null()]),
address_country: z.union([z.string(), z.null()]),
phone: z.union([z.string(), z.null()]),
email: z.union([z.string(), z.null()]),
is_archived: z.boolean(),
archived_at: z.union([z.string(), z.null()]),
invoices_count: z.number().int(),
has_non_draft_invoices: z.boolean(),
created_at: z.union([z.string(), z.null()]),
updated_at: z.union([z.string(), z.null()]),
})
.passthrough();
const InvoiceRecipientCollection = z.array(InvoiceRecipientResource);
const InvoiceRecipientRequest = z
.object({
name: z.string(),
vatin: z.union([z.string(), z.null()]).optional(),
address_line_1: z.union([z.string(), z.null()]).optional(),
address_line_2: z.union([z.string(), z.null()]).optional(),
address_line_3: z.union([z.string(), z.null()]).optional(),
address_post_code: z.union([z.string(), z.null()]).optional(),
address_city: z.union([z.string(), z.null()]).optional(),
address_country: z.union([z.string(), z.null()]).optional(),
phone: z.union([z.string(), z.null()]).optional(),
email: z.union([z.string(), z.null()]).optional(),
is_archived: z.boolean().optional(),
})
.passthrough();
const InvoiceResource = z
.object({
id: z.string(),
organization_id: z.string(),
invoice_recipient_id: z.string(),
reference: z.string(),
seller_name: z.string(),
buyer_name: z.string(),
recipient: z.string(),
status: z.string(),
status_label: z.string(),
date: z.string(),
due_at: z.string(),
paid_date: z.string(),
@@ -76,16 +116,7 @@ const InvoiceStoreRequest = z
seller_address_country: z.union([z.string(), z.null()]).optional(),
seller_phone: z.union([z.string(), z.null()]).optional(),
seller_email: z.union([z.string(), z.null()]).optional(),
buyer_name: z.string(),
buyer_vatin: z.union([z.string(), z.null()]).optional(),
buyer_address_line_1: z.union([z.string(), z.null()]).optional(),
buyer_address_line_2: z.union([z.string(), z.null()]).optional(),
buyer_address_line_3: z.union([z.string(), z.null()]).optional(),
buyer_address_post_code: z.union([z.string(), z.null()]).optional(),
buyer_address_city: z.union([z.string(), z.null()]).optional(),
buyer_address_country: z.union([z.string(), z.null()]).optional(),
buyer_phone: z.union([z.string(), z.null()]).optional(),
buyer_email: z.union([z.string(), z.null()]).optional(),
invoice_recipient_id: z.string(),
date: z.string(),
billing_period_start: z.union([z.string(), z.null()]).optional(),
billing_period_end: z.union([z.string(), z.null()]).optional(),
@@ -130,6 +161,7 @@ const DetailedInvoiceResource = z
.object({
id: z.string(),
organization_id: z.string(),
invoice_recipient_id: z.string(),
reference: z.string(),
seller_name: z.string(),
seller_vatin: z.string(),
@@ -141,16 +173,7 @@ const DetailedInvoiceResource = z
seller_address_country: z.string(),
seller_phone: z.string(),
seller_email: z.string(),
buyer_name: z.string(),
buyer_vatin: z.string(),
buyer_address_line_1: z.string(),
buyer_address_line_2: z.string(),
buyer_address_line_3: z.string(),
buyer_address_post_code: z.string(),
buyer_address_city: z.string(),
buyer_address_country: z.string(),
buyer_phone: z.string(),
buyer_email: z.string(),
recipient: InvoiceRecipientResource,
paid_date: z.string(),
due_at: z.string(),
discount_type: z.string(),
@@ -171,7 +194,7 @@ const DetailedInvoiceResource = z
entries: z.array(InvoiceEntryResource),
})
.passthrough();
const InvoiceStatus = z.enum(['draft', 'sent', 'cancelled']);
const InvoiceStatus = z.enum(['draft', 'sent', 'paid', 'cancelled']);
const InvoiceUpdateRequest = z
.object({
status: InvoiceStatus,
@@ -187,16 +210,7 @@ const InvoiceUpdateRequest = z
seller_address_country: z.union([z.string(), z.null()]),
seller_phone: z.union([z.string(), z.null()]),
seller_email: z.union([z.string(), z.null()]),
buyer_name: z.string(),
buyer_vatin: z.union([z.string(), z.null()]),
buyer_address_line_1: z.union([z.string(), z.null()]),
buyer_address_line_2: z.union([z.string(), z.null()]),
buyer_address_line_3: z.union([z.string(), z.null()]),
buyer_address_post_code: z.union([z.string(), z.null()]),
buyer_address_city: z.union([z.string(), z.null()]),
buyer_address_country: z.union([z.string(), z.null()]),
buyer_phone: z.union([z.string(), z.null()]),
buyer_email: z.union([z.string(), z.null()]),
invoice_recipient_id: z.string(),
date: z.string(),
billing_period_start: z.union([z.string(), z.null()]),
billing_period_end: z.union([z.string(), z.null()]),
@@ -1885,6 +1899,125 @@ const endpoints = makeApi([
},
],
},
{
method: 'get',
path: '/v1/organizations/:organization/invoice-recipients',
alias: 'getInvoiceRecipients',
requestFormat: 'json',
parameters: [
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientCollection }).passthrough(),
},
{
method: 'post',
path: '/v1/organizations/:organization/invoice-recipients',
alias: 'createInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: InvoiceRecipientRequest,
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'get',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
alias: 'getInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'put',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
alias: 'updateInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: InvoiceRecipientRequest,
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'post',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient/duplicate',
alias: 'duplicateInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: InvoiceRecipientRequest,
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'delete',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
alias: 'deleteInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.void(),
},
{
method: 'get',
path: '/v1/organizations/:organization/invoices',
@@ -1901,6 +2034,11 @@ const endpoints = makeApi([
type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(),
},
{
name: 'status',
type: 'Query',
schema: InvoiceStatus.optional(),
},
],
response: z.object({ data: InvoiceCollection }).passthrough(),
errors: [

View File

@@ -6,9 +6,7 @@
"compilerOptions": {
"paths": {
"@/*": ["./resources/js/*"],
"@solidtime/ui": ["./resources/js/packages/ui/src/index.ts"],
"@e2e/*": ["./e2e/*"],
"@e2e-support/*": ["./playwright/*"]
"@solidtime/ui": ["./resources/js/packages/ui/src/index.ts"]
}
},
"skipLibCheck": true,