Skip to content

Commit 29867a6

Browse files
committed
Include tax residency attestation in extras
1 parent 3e6a36b commit 29867a6

File tree

13 files changed

+128
-63
lines changed

13 files changed

+128
-63
lines changed

prisma/schema.prisma

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ enum CredentialType {
1919
PID
2020
DIPLOMA
2121
SEAFARER
22-
BOTH
22+
TAXRESIDENCY
2323
}
2424

2525
model JobPosting {
@@ -101,4 +101,4 @@ model VerifiedCredential {
101101
@@index([applicationId])
102102
@@index([verifierTransactionId])
103103
@@index([applicationId, credentialType])
104-
}
104+
}

src/app/api/applications/[id]/extras/route.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CredentialType } from '@prisma/client';
12
import { NextResponse } from 'next/server';
23

34
import { Container } from '@/server';
@@ -13,23 +14,26 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
1314

1415
const diploma = json.diploma ?? false;
1516
const seafarer = json.seafarer ?? false;
17+
const taxResidency = json.taxResidency ?? false;
1618
const sameDeviceFlow = json.sameDeviceFlow ?? false;
1719

18-
if (!diploma && !seafarer) {
20+
if (!diploma && !seafarer && !taxResidency) {
1921
return NextResponse.json(
2022
{ error: 'At least one credential type must be selected' },
2123
{ status: 400 },
2224
);
2325
}
2426

2527
// Determine credential type based on checkboxes
26-
let credentialType: 'DIPLOMA' | 'SEAFARER' | 'BOTH';
27-
if (diploma && seafarer) {
28-
credentialType = 'BOTH';
29-
} else if (diploma) {
30-
credentialType = 'DIPLOMA';
31-
} else {
32-
credentialType = 'SEAFARER';
28+
const credentialType: CredentialType[] = [];
29+
if (diploma) {
30+
credentialType.push(CredentialType.DIPLOMA);
31+
}
32+
if (seafarer) {
33+
credentialType.push(CredentialType.SEAFARER);
34+
}
35+
if (taxResidency) {
36+
credentialType.push(CredentialType.TAXRESIDENCY);
3337
}
3438

3539
const result = await applicationService.requestAdditionalCredentials({

src/app/applications/[id]/extras/page.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,30 @@ export default async function ApplicationExtrasPage({
3030
const verifiedCredentials = await applicationService.getVerifiedCredentials(id);
3131
const extrasCredentials = verifiedCredentials.filter(
3232
(c) =>
33-
(c.credentialType === 'DIPLOMA' || c.credentialType === 'SEAFARER') && c.status === 'PENDING',
33+
(c.credentialType === 'DIPLOMA' ||
34+
c.credentialType === 'SEAFARER' ||
35+
c.credentialType === 'TAXRESIDENCY') &&
36+
c.status === 'PENDING',
3437
);
3538

3639
if (extrasCredentials.length === 0) return notFound();
3740

3841
// Determine what was requested based on credentials
3942
const hasDiploma = extrasCredentials.some((c) => c.credentialType === 'DIPLOMA');
4043
const hasSeafarer = extrasCredentials.some((c) => c.credentialType === 'SEAFARER');
44+
const hasTaxResidency = extrasCredentials.some((c) => c.credentialType === 'TAXRESIDENCY');
4145

42-
let extrasCredentialTypeLabel: string;
43-
if (hasDiploma && hasSeafarer) {
44-
extrasCredentialTypeLabel = 'Diploma & Seafarer Certificate';
45-
} else if (hasDiploma) {
46-
extrasCredentialTypeLabel = 'Diploma';
47-
} else {
48-
extrasCredentialTypeLabel = 'Seafarer Certificate';
46+
const certificates: string[] = [];
47+
if (hasDiploma) {
48+
certificates.push('Diploma');
4949
}
50+
if (hasSeafarer) {
51+
certificates.push('Seafarer');
52+
}
53+
if (hasTaxResidency) {
54+
certificates.push('Tax Residency');
55+
}
56+
const extrasCredentialTypeLabel = certificates.join(' & ') + ' Certificate';
5057

5158
const title = app.job?.title ?? 'Application';
5259

src/app/jobs/[id]/page.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,10 @@ export default async function JobDetailsPage({ params }: { params: Promise<{ id:
150150
Optional Credentials:
151151
</Typography>
152152
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
153-
{(job.getRequiredCredentials() === 'DIPLOMA' ||
154-
job.getRequiredCredentials() === 'BOTH') && (
153+
{job.getRequiredCredentials() === 'DIPLOMA' && (
155154
<Chip color="primary" variant="outlined" label="Diploma (optional)" />
156155
)}
157-
{(job.getRequiredCredentials() === 'SEAFARER' ||
158-
job.getRequiredCredentials() === 'BOTH') && (
156+
{job.getRequiredCredentials() === 'SEAFARER' && (
159157
<Chip
160158
color="primary"
161159
variant="outlined"

src/components/atoms/AdditionalInfoActions.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ import LogoBox from './LogoBox';
1111
export default function AdditionalInfoActions({ applicationId }: { applicationId: string }) {
1212
const [diploma, setDiploma] = useState(false);
1313
const [seafarer, setSeafarer] = useState(false);
14+
const [taxResidency, setTaxResidency] = useState(false);
1415
const [busy, setBusy] = useState<'provide' | 'finalise' | null>(null);
1516

16-
const disabled = !diploma && !seafarer;
17+
const disabled = !diploma && !seafarer && !taxResidency;
1718

1819
const provideExtrasCrossDevice = async () => {
1920
setBusy('provide');
2021
try {
2122
const response = await fetch(`/api/applications/${applicationId}/extras`, {
2223
method: 'POST',
2324
headers: { 'Content-Type': 'application/json' },
24-
body: JSON.stringify({ diploma, seafarer, sameDeviceFlow: false }),
25+
body: JSON.stringify({ diploma, seafarer, taxResidency, sameDeviceFlow: false }),
2526
});
2627

2728
if (!response.ok) {
@@ -48,7 +49,7 @@ export default function AdditionalInfoActions({ applicationId }: { applicationId
4849
const response = await fetch(`/api/applications/${applicationId}/extras`, {
4950
method: 'POST',
5051
headers: { 'Content-Type': 'application/json' },
51-
body: JSON.stringify({ diploma, seafarer, sameDeviceFlow: true }),
52+
body: JSON.stringify({ diploma, seafarer, taxResidency, sameDeviceFlow: true }),
5253
});
5354

5455
if (!response.ok) {
@@ -102,6 +103,12 @@ export default function AdditionalInfoActions({ applicationId }: { applicationId
102103
control={<Checkbox checked={seafarer} onChange={(e) => setSeafarer(e.target.checked)} />}
103104
label="Seafarer Certificate"
104105
/>
106+
<FormControlLabel
107+
control={
108+
<Checkbox checked={taxResidency} onChange={(e) => setTaxResidency(e.target.checked)} />
109+
}
110+
label="Tax Residency"
111+
/>
105112
</Stack>
106113

107114
<Stack spacing={2} alignItems="center">

src/components/atoms/CredentialRequirementChips.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export default function CredentialRequirementChips({
2121
return null;
2222
}
2323

24-
const showDiploma = requiredCredentials === 'DIPLOMA' || requiredCredentials === 'BOTH';
25-
const showSeafarer = requiredCredentials === 'SEAFARER' || requiredCredentials === 'BOTH';
24+
const showDiploma = requiredCredentials === 'DIPLOMA';
25+
const showSeafarer = requiredCredentials === 'SEAFARER';
2626

2727
return (
2828
<Stack direction="row" spacing={1} alignItems="center">

src/server/domain/entities/Job.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ export class Job {
4040
* Business Rule: Determines if this job requires diploma credentials
4141
*/
4242
requiresDiploma(): boolean {
43-
return this.requiredCredentials === 'DIPLOMA' || this.requiredCredentials === 'BOTH';
43+
return this.requiredCredentials === 'DIPLOMA';
4444
}
4545

4646
/**
4747
* Business Rule: Determines if this job requires seafarer certificate
4848
*/
4949
requiresSeafarerCert(): boolean {
50-
return this.requiredCredentials === 'SEAFARER' || this.requiredCredentials === 'BOTH';
50+
return this.requiredCredentials === 'SEAFARER';
5151
}
5252

5353
/**
@@ -60,15 +60,15 @@ export class Job {
6060
/**
6161
* Business Rule: Validate if candidate meets job requirements
6262
*/
63-
candidateMeetsRequirements(hasDiploma: boolean, hasSeafarerCert: boolean): boolean {
63+
candidateMeetsRequirements(
64+
hasDiploma: boolean,
65+
hasSeafarerCert: boolean,
66+
hasTaxResidency: boolean,
67+
): boolean {
6468
if (!this.hasCredentialRequirements()) {
6569
return true; // No requirements, always qualified
6670
}
6771

68-
if (this.requiredCredentials === 'BOTH') {
69-
return hasDiploma && hasSeafarerCert;
70-
}
71-
7272
if (this.requiredCredentials === 'DIPLOMA') {
7373
return hasDiploma;
7474
}
@@ -77,6 +77,10 @@ export class Job {
7777
return hasSeafarerCert;
7878
}
7979

80+
if (this.requiredCredentials === 'TAXRESIDENCY') {
81+
return hasTaxResidency;
82+
}
83+
8084
return true;
8185
}
8286

src/server/domain/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
* - PID: Personal Identification Document
99
* - DIPLOMA: Educational diploma certificate
1010
* - SEAFARER: Seafarer certificate
11-
* - BOTH: Both diploma and seafarer certificates
11+
* - TAXRESIDENCY: Tax Residency certificate
1212
*/
13-
export type CredentialType = 'NONE' | 'PID' | 'DIPLOMA' | 'SEAFARER' | 'BOTH';
13+
export type CredentialType = 'NONE' | 'PID' | 'DIPLOMA' | 'SEAFARER' | 'TAXRESIDENCY';

src/server/schemas/application.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod';
2+
import {CredentialType} from "@prisma/client";
23

34
export const applicationCreateSchema = z.object({
45
jobId: z.string().min(1),
@@ -14,7 +15,7 @@ export const applicationVerificationSchema = z.object({
1415

1516
export const applicationExtrasSchema = z.object({
1617
applicationId: z.string().min(1),
17-
credentialType: z.enum(['DIPLOMA', 'SEAFARER', 'BOTH']),
18+
credentialType: z.array(z.enum(CredentialType)),
1819
sameDeviceFlow: z.boolean(),
1920
});
2021

src/server/services/ApplicationService.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { JobRepository } from '@/server/repositories/JobRepository';
99
import { VerifiedCredentialRepository } from '@/server/repositories/VerifiedCredentialRepository';
1010
import {
1111
applicationCreateSchema,
12+
applicationExtrasSchema,
1213
applicationIdSchema,
1314
applicationVerificationSchema,
14-
applicationExtrasSchema,
1515
} from '@/server/schemas/application';
1616

1717
import { IssuerService } from './IssuerService';
@@ -65,7 +65,7 @@ export class ApplicationService {
6565
const initVerificationResponse = await this.verifier.initVerification(
6666
application.getId(),
6767
newRequesParamsFor.sameDeviceFlow,
68-
'PID',
68+
['PID'],
6969
);
7070

7171
// Create PENDING VerifiedCredential record for PID
@@ -128,7 +128,9 @@ export class ApplicationService {
128128
const extrasCredentials = await this.verifiedCredentialRepo.findByApplicationId(applicationId);
129129
const extrasCredential = extrasCredentials.find(
130130
(c) =>
131-
(c.credentialType === 'DIPLOMA' || c.credentialType === 'SEAFARER') &&
131+
(c.credentialType === 'DIPLOMA' ||
132+
c.credentialType === 'SEAFARER' ||
133+
c.credentialType === 'TAXRESIDENCY') &&
132134
c.status === 'PENDING',
133135
);
134136

@@ -264,7 +266,7 @@ export class ApplicationService {
264266
@ValidateInput(applicationExtrasSchema)
265267
public async requestAdditionalCredentials(data: {
266268
applicationId: string;
267-
credentialType: 'DIPLOMA' | 'SEAFARER' | 'BOTH';
269+
credentialType: CredentialType[];
268270
sameDeviceFlow: boolean;
269271
}): Promise<{ url: string }> {
270272
const { applicationId, credentialType, sameDeviceFlow } = data;
@@ -289,20 +291,31 @@ export class ApplicationService {
289291
// Create PENDING VerifiedCredential records for requested credentials
290292
const credentialsToCreate: Array<{ type: CredentialType; namespace: string }> = [];
291293

292-
if (credentialType === 'DIPLOMA' || credentialType === 'BOTH') {
294+
console.log(credentialType);
295+
296+
if (credentialType.includes('DIPLOMA')) {
293297
credentialsToCreate.push({
294298
type: 'DIPLOMA',
295299
namespace: 'urn:eu.europa.ec.eudi:diploma:1:1',
296300
});
297301
}
298302

299-
if (credentialType === 'SEAFARER' || credentialType === 'BOTH') {
303+
if (credentialType.includes('SEAFARER')) {
300304
credentialsToCreate.push({
301305
type: 'SEAFARER',
302306
namespace: 'eu.europa.ec.eudi.seafarer.1',
303307
});
304308
}
305309

310+
if (credentialType.includes('TAXRESIDENCY')) {
311+
credentialsToCreate.push({
312+
type: 'TAXRESIDENCY',
313+
namespace: 'urn:eu.europa.ec.eudi:tax:1:1',
314+
});
315+
}
316+
317+
console.log(credentialsToCreate);
318+
306319
// Create PENDING records for each credential
307320
for (const { type, namespace } of credentialsToCreate) {
308321
await this.verifiedCredentialRepo.create({
@@ -334,7 +347,9 @@ export class ApplicationService {
334347
const extrasCredentials = await this.verifiedCredentialRepo.findByApplicationId(applicationId);
335348
const extrasCredential = extrasCredentials.find(
336349
(c) =>
337-
(c.credentialType === 'DIPLOMA' || c.credentialType === 'SEAFARER') &&
350+
(c.credentialType === 'DIPLOMA' ||
351+
c.credentialType === 'SEAFARER' ||
352+
c.credentialType === 'TAXRESIDENCY') &&
338353
c.status === 'PENDING',
339354
);
340355

0 commit comments

Comments
 (0)