-
Notifications
You must be signed in to change notification settings - Fork 1
feat(customer): enforce unique phoneNumber with validation, duplicate handling & cleanup #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(customer): enforce unique phoneNumber with validation, duplicate handling & cleanup #18
Conversation
… handling & cleanup
Completed Working on "Code Review"✅ Code review process completed: All 10 items reviewed, inline comments added, and overall summary submitted (COMMENT). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of review (action required):
- Schema/DTO inconsistency –
phoneNumberis@uniquebut still nullable in Prisma while DTOs/tests treat it as required. This weakens uniqueness and can cause runtime errors. - Error handling –
customer.servicemaps everyP2002toDuplicatePhoneNumberError; checkerror.meta.targetto avoid misclassification and sanitize unexpected errors. - Security – DefaultAuthGuard always returns
true; ACL interceptors are empty, effectively disabling auth/ACL. Ensure they’re implemented or not registered in production. - Cleanup script – Runs
findManythendeleteManyoutside a transaction, risking race conditions and high memory usage; wrap in transaction or optimize query. - Tests – Add negative test confirming non-phone unique violations aren’t turned into
DuplicatePhoneNumberError. - package.json – Move
ts-nodeandnest-access-controlto devDependencies if not used at runtime; update docs to clarify nullable uniqueness caveat and safe script usage.
Please address these points before merging.
| id String @id @default(cuid()) | ||
| lastName String? | ||
| phoneNumber String? | ||
| phoneNumber String? @unique |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Correctness / Data-Integrity
phoneNumber is now marked @unique, but it remains nullable (String?). In most relational databases (e.g. PostgreSQL) a UNIQUE constraint still allows multiple NULL values, meaning the schema will not prevent inserting several customers without a phone number.
Because the DTO layer was changed to require a non-empty phone number, keeping the column nullable introduces a mismatch between the application contract and the database and leaves room for unexpected NULL values via:
- Legacy data or direct DB access scripts
- Future migrations/tests that bypass the validation layer
If every customer must have a phone number and it must be unique, consider dropping the ? to make the column non-nullable:
phoneNumber String @uniqueThis guarantees consistency at the storage layer and aligns with the new validation decorators.
If there is a transitional period where NULL values must persist, please document the plan and add a follow-up migration to set NOT NULL once the cleanup script has removed existing gaps.
| @Matches(/^[0-9]{7,15}$/) | ||
| @Field(() => String, { | ||
| nullable: true, | ||
| nullable: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nullable: false makes the field mandatory at the GraphQL / DTO layer, but the underlying Prisma schema still defines phoneNumber as optional (String? @unique). This mismatch means:
- The database will still accept
NULLvalues (and multiple rows withNULL), so the uniqueness guarantee you intend to enforce can be silently bypassed by callers that do not go through this DTO (e.g. seeds, scripts, future mutations). - Runtime code that relies on the Prisma type may still pass
null/undefined, leading to inconsistent data or unexpectedP2002errors only in some paths.
Either make the column non-nullable in Prisma (String @unique) or keep the DTO nullable and enforce validation only when a value is provided, so both layers stay consistent.
| error && | ||
| typeof error === "object" && | ||
| "code" in error && | ||
| (error as any).code === "P2002" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Correctness & Maintainability
P2002 is Prisma’s generic unique-constraint violation. Multiple columns in the Customer table may be subject to @unique (e.g. id, any future email, etc.).
Relying only on error.code === "P2002" will therefore turn all unique-constraint collisions into a DuplicatePhoneNumberError, potentially leaking the wrong information to callers and hiding other integrity issues.
Prisma includes the conflicting columns in error.meta?.target – you can safely disambiguate with:
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === "P2002" &&
Array.isArray(error.meta?.target) &&
error.meta.target.includes("phoneNumber")
) {
throw new DuplicatePhoneNumberError();
}This also removes the any cast and keeps strict typing.
Consider updating the guard accordingly so only phoneNumber collisions are mapped to DuplicatePhoneNumberError.
| if ( | ||
| error && | ||
| typeof error === "object" && | ||
| "code" in error && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔐 Security / Information Leakage
Using throw error; re-throws the raw Prisma error back up the call stack, which eventually propagates to Nest’s HTTP exception filter. Unless you have a global filter that sanitises Prisma errors, this may leak internal details (database column names, constraint identifiers) to API consumers.
Consider mapping all unexpected errors to a generic InternalServerErrorException (or re-use an existing domain error) instead of exposing driver specifics.
| const removeIds = customers.slice(1).map((c) => c.id); | ||
|
|
||
| // Delete the duplicates (excluding the one we keep) | ||
| const { count } = await prisma.customer.deleteMany({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider wrapping the selection + deletion steps inside a single prisma.$transaction([...]) call.
Without a transaction, any concurrent writes between the findMany (lines 55-59) and this deleteMany could introduce race-conditions where:
- a new customer with the same phone number is inserted after
findManyruns, or - another process deletes/updates one of the records you plan to keep/remove.
Enclosing both operations in a transaction guarantees atomicity and prevents accidental data loss under load.
| @Injectable() | ||
| export class AclValidateRequestInterceptor implements NestInterceptor { | ||
| intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
| // In production, this would validate request against ACL rules. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this interceptor currently performs no validation and simply forwards the request, registering it globally may give a false sense of security and mask missing ACL checks. Until full logic is implemented, consider:
- guarding registration with a feature flag,
- logging a warning when invoked, or
- at minimum, documenting clearly that it is a no-op.
Otherwise, reviewers might assume requests are validated when they are not.
| @Injectable() | ||
| export class AclFilterResponseInterceptor implements NestInterceptor { | ||
| intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
| // In production, this would filter response based on ACL rules. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the request-validation interceptor, this response filter currently does nothing. If consumers rely on it to mask restricted fields, sensitive data could leak. Until implemented, you might:
- leave the class unregistered,
- throw
NotImplementedExceptionwhen invoked, or - include a TODO comment with a link to the tracking ticket to avoid accidental production exposure.
README.md
Outdated
|
|
||
| The Prisma schema is located under `apps/hotel-management-service-server/prisma/schema.prisma`. | ||
|
|
||
| > **Note**: Starting from the next release the `phoneNumber` field on the **Customer** model is **unique**. Attempting to insert two customers with the same phone number will result in an HTTP 409 Conflict error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The note accurately states that phoneNumber is now unique, but it does not mention that the underlying Prisma field is still nullable (String? @unique). Because most SQL dialects allow multiple NULL values under a unique constraint, it is still possible to insert many customers whose phoneNumber is null, silently bypassing the uniqueness guarantee you describe here. Either:
- Update the schema to make
phoneNumbernon-nullable, or - Amend this note to clarify that the uniqueness guarantee only applies to non-null values and highlight the expectation that
phoneNumberis always provided at the API level.
Documenting this caveat will prevent confusion and production data inconsistencies.
| > | ||
| > ```bash | ||
| > cd apps/hotel-management-service-server | ||
| > npm run cleanup:duplicates # removes duplicates, keeping the earliest record per phone number |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding --preview-feature in the command (if required by your Prisma version) or specifying the Node environment (e.g. NODE_ENV=script) so that the script connects to the correct database. Without clear guidance, teams might accidentally run this against production before verifying in staging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@overcut i accept that , please add a commit with this change on this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ## [Unreleased] | ||
| ### Added | ||
| - **Database**: Added a unique constraint on `phoneNumber` column of the `Customer` table (Prisma schema & migration). | ||
| - **API**: Implemented application-level handling for duplicate phone numbers – requests that attempt to create a customer with an existing `phoneNumber` now return **HTTP 409 Conflict** (error code `DuplicatePhoneNumberError`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor but important: the error class is named DuplicatePhoneNumberError server-side, but the client receives the standardized error code DUPLICATE_PHONE_NUMBER. Aligning the wording here (or adding the literal string) makes it easier for frontend consumers scanning the changelog to know what to guard for.
Completed Working on "Auto-Changelog Reminder"✅ Reviewed PR for changelog requirement: user-facing changes detected, changelog modification present. Posted acknowledgment comment. |
|
✅ Changelog entry detected. Thank you! |
|
📌 Change applied: Updated:
Let me know if you’d like further tweaks – otherwise we’re good to merge. 🙌 I’ll keep this context open in this thread - reply with |

Resolves #17
Overall goal:
Ensure customer phone numbers remain unique across the system and gracefully handle attempts to create duplicates.
Checklist / Implemented Phases:
@uniqueonCustomer.phoneNumber+ migration generated.P2002(duplicate) mapped toDuplicatePhoneNumberErrorreturning HTTP 409.npm run cleanup:duplicates) + package.json script & README instructions.Notes for reviewers:
a. Execute
npm run cleanup:duplicatesto purge existing duplicate phone numbers.b. Apply database migrations (Prisma migrate deploy) to add the unique constraint.
npm test).