Skip to content

Commit

Permalink
feat(form-service): set unique constraint on oneFormPerApplicant config
Browse files Browse the repository at this point in the history
Support for form definitions that permit multiple forms per applicant (when setting is set to false).
  • Loading branch information
tzuge committed Jan 30, 2025
1 parent c790133 commit bb5dcd3
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 8 deletions.
1 change: 1 addition & 0 deletions apps/form-service/src/form/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const configurationSchema = {
description: { type: 'string' },
formDraftUrlTemplate: { type: 'string', pattern: '^http[s]?://.{0,500}$' },
anonymousApply: { type: 'boolean' },
oneFormPerApplicant: { type: 'boolean', default: true },
applicantRoles: { type: 'array', items: { type: 'string' } },
assessorRoles: { type: 'array', items: { type: 'string' } },
clerkRoles: { type: 'array', items: { type: 'string' } },
Expand Down
4 changes: 4 additions & 0 deletions apps/form-service/src/form/model/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class FormDefinitionEntity implements FormDefinition {
name: string;
description: string;
anonymousApply: boolean;
oneFormPerApplicant: boolean;
applicantRoles: string[];
assessorRoles: string[];
clerkRoles: string[];
Expand All @@ -40,6 +41,9 @@ export class FormDefinitionEntity implements FormDefinition {
this.name = definition.name;
this.description = definition.description;
this.anonymousApply = definition.anonymousApply || false;
// Defaults to true if the configuration value is not set.
this.oneFormPerApplicant =
typeof definition.oneFormPerApplicant === 'boolean' ? definition.oneFormPerApplicant : true;
this.applicantRoles = definition.applicantRoles || [];
this.assessorRoles = definition.assessorRoles || [];
this.clerkRoles = definition.clerkRoles || [];
Expand Down
1 change: 1 addition & 0 deletions apps/form-service/src/form/types/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface FormDefinition {
name: string;
description: string;
anonymousApply: boolean;
oneFormPerApplicant?: boolean;
applicantRoles: string[];
assessorRoles: string[];
clerkRoles: string[];
Expand Down
23 changes: 16 additions & 7 deletions apps/form-service/src/mongo/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Model, model } from 'mongoose';
import { Logger } from 'winston';
import { FormCriteria, FormEntity, FormRepository } from '../form';
import { FormDefinitionRepository } from '../form';
import { NotificationService } from '../notification';
import { NotificationService, Subscriber } from '../notification';
import { formSchema } from './schema';
import { FormDoc } from './types';

Expand Down Expand Up @@ -135,7 +135,12 @@ export class MongoFormRepository implements FormRepository {
// NOTE: This is only set on insert (create).
// The UUID is necessary due to backwards compatibility with a unique index on tenant, definition, and applicant IDs.
// Setting form ID makes the unique context effectively tenant, definition, form IDs.
applicantId: entity.applicant?.urn.toString() || entity.id,
// Setting the applicant URN restricts to one per applicant.
applicantId:
entity.definition?.oneFormPerApplicant && entity.applicant
? entity.applicant.urn.toString() // Creating user ID might be better, but that can be an intake service account.
: entity.id,
subscriberId: entity.applicant?.urn.toString(),
status: entity.status,
created: entity.created,
createdBy: entity.createdBy,
Expand All @@ -156,11 +161,15 @@ export class MongoFormRepository implements FormRepository {
const tenantId = AdspId.parse(doc.tenantId);
const definition = await this.definitionRepository.getDefinition(tenantId, doc.definitionId);

// NOTE: The applicant ID is a UUID in case there's no associated Subscriber;
// this is necessary for backwards compatibility with index.
const applicant = AdspId.isAdspId(doc.applicantId)
? await this.notificationService.getSubscriber(tenantId, AdspId.parse(doc.applicantId))
: null;
let applicant: Subscriber;
if (AdspId.isAdspId(doc.subscriberId)) {
applicant = await this.notificationService.getSubscriber(tenantId, AdspId.parse(doc.subscriberId));
} else if (AdspId.isAdspId(doc.applicantId)) {
// NOTE: The applicant ID is a UUID in case there's no associated Subscriber;
// this is necessary for backwards compatibility with index.
applicant = await this.notificationService.getSubscriber(tenantId, AdspId.parse(doc.applicantId));
}

return new FormEntity(
this,
tenantId,
Expand Down
6 changes: 5 additions & 1 deletion apps/form-service/src/mongo/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ export const formSchema = new Schema(
type: Boolean,
default: false,
},

// Applicant URN no longer stored here, except for in old form records.
// Instead this property is used for the uniqueness constraint on application user ID.
applicantId: {
type: String,
required: true,
},
subscriberId: {
type: String,
},
created: {
type: Date,
required: true,
Expand Down
2 changes: 2 additions & 0 deletions apps/form-service/src/mongo/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ export type FormDoc = Omit<Form, 'definition' | 'applicant' | 'files'> & {
tenantId: string;
definitionId: string;
applicantId: string;
subscriberId: string;
hash: string;
files: Record<string, string>;
};

export type FormSubmissionDoc = Omit<FormSubmission, 'updatedBy' | 'updated' | 'formFiles'> & {
tenantId: string;
hash: string;
Expand Down

0 comments on commit bb5dcd3

Please sign in to comment.