feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
|
|
|
/**
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
* Public interest creation - extracted from `/api/public/interests/route.ts`
|
feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
|
|
|
* per the C.4 audit finding ("Public POST routes bypass service layer"). The
|
|
|
|
|
* pre-extraction route was 346 lines of inline DB logic + audit + email
|
|
|
|
|
* fan-out, which made unit testing the dedup, ownership, and address rules
|
|
|
|
|
* effectively impossible without spinning up a full HTTP request fixture.
|
|
|
|
|
*
|
|
|
|
|
* After extraction:
|
|
|
|
|
* - The route handles HTTP concerns: rate-limit, port resolution from
|
|
|
|
|
* headers, parseBody validation, audit-log + email side-effect dispatch.
|
|
|
|
|
* - This service handles the transactional trio creation (client + yacht
|
|
|
|
|
* + interest, plus optional company + membership + address).
|
|
|
|
|
*
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
* The companion routes - `/api/public/website-inquiries/route.ts` (pure raw
|
feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
|
|
|
* capture; no entity creation) and `/api/public/residential-inquiries/route.ts`
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
* (residential funnel, separate schema) - were intentionally NOT extracted
|
feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
|
|
|
* here. Their bodies are smaller and their concerns don't overlap with the
|
|
|
|
|
* marina-funnel logic this service encapsulates.
|
|
|
|
|
*/
|
|
|
|
|
import { and, eq, isNull, sql } from 'drizzle-orm';
|
|
|
|
|
|
|
|
|
|
import { db } from '@/lib/db';
|
|
|
|
|
import { withTransaction } from '@/lib/db/utils';
|
|
|
|
|
import { interests, interestBerths } from '@/lib/db/schema/interests';
|
|
|
|
|
import { clients, clientContacts, clientAddresses } from '@/lib/db/schema/clients';
|
|
|
|
|
import { berths } from '@/lib/db/schema/berths';
|
|
|
|
|
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema/yachts';
|
|
|
|
|
import { companies, companyMemberships } from '@/lib/db/schema/companies';
|
|
|
|
|
import { parsePhone } from '@/lib/i18n/phone';
|
|
|
|
|
import type { CountryCode } from '@/lib/i18n/countries';
|
|
|
|
|
import type { publicInterestSchema } from '@/lib/validators/interests';
|
|
|
|
|
import type { z } from 'zod';
|
|
|
|
|
|
|
|
|
|
type PublicInterestData = z.infer<typeof publicInterestSchema>;
|
|
|
|
|
type Tx = typeof db;
|
|
|
|
|
|
|
|
|
|
export interface CreatePublicInterestArgs {
|
|
|
|
|
portId: string;
|
|
|
|
|
data: PublicInterestData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CreatePublicInterestResult {
|
|
|
|
|
interestId: string;
|
|
|
|
|
clientId: string;
|
|
|
|
|
yachtId: string;
|
|
|
|
|
companyId: string | null;
|
|
|
|
|
berthId: string | null;
|
|
|
|
|
resolvedMooringNumber: string | null;
|
|
|
|
|
fullName: string;
|
|
|
|
|
firstName: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createPublicInterest(
|
|
|
|
|
args: CreatePublicInterestArgs,
|
|
|
|
|
): Promise<CreatePublicInterestResult> {
|
|
|
|
|
const { portId, data } = args;
|
|
|
|
|
|
|
|
|
|
// Server-side phone normalization for older website builds that post raw
|
|
|
|
|
// international/national strings. Newer builds may pre-fill phoneE164/Country.
|
|
|
|
|
let phoneE164 = data.phoneE164 ?? null;
|
|
|
|
|
let phoneCountry: CountryCode | null = (data.phoneCountry as CountryCode | null) ?? null;
|
|
|
|
|
if (!phoneE164) {
|
|
|
|
|
const parsed = parsePhone(data.phone, phoneCountry ?? undefined);
|
|
|
|
|
phoneE164 = parsed.e164;
|
|
|
|
|
phoneCountry = parsed.country ?? phoneCountry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fullName =
|
|
|
|
|
data.firstName && data.lastName
|
|
|
|
|
? `${data.firstName} ${data.lastName}`
|
|
|
|
|
: (data.fullName ?? 'Unknown');
|
|
|
|
|
|
|
|
|
|
const firstName = data.firstName ?? fullName.split(/\s+/)[0] ?? 'Valued Guest';
|
|
|
|
|
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
// Resolve berth by mooring number (if provided). Read-only lookup - safe
|
feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
|
|
|
// to do outside the transaction.
|
|
|
|
|
let berthId: string | null = null;
|
|
|
|
|
let resolvedMooringNumber: string | null = data.mooringNumber ?? null;
|
|
|
|
|
if (data.mooringNumber) {
|
|
|
|
|
const berth = await db.query.berths.findFirst({
|
|
|
|
|
where: and(eq(berths.mooringNumber, data.mooringNumber), eq(berths.portId, portId)),
|
|
|
|
|
});
|
|
|
|
|
if (berth) {
|
|
|
|
|
berthId = berth.id;
|
|
|
|
|
resolvedMooringNumber = berth.mooringNumber;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Transactional trio creation ────────────────────────────────────────
|
|
|
|
|
const result = await withTransaction(async (tx) => {
|
|
|
|
|
// 1. Find or create client by email. The inquiry-funnel audit
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
// flagged that the previous exact match was case-sensitive -
|
feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
|
|
|
// capital-letter resubmissions spawned duplicate client+yacht+
|
|
|
|
|
// interest rows. Match LOWER(value) instead so foo@x.com and
|
|
|
|
|
// Foo@X.COM dedupe to the same client.
|
|
|
|
|
let clientId: string;
|
|
|
|
|
const normalizedEmail = data.email.trim().toLowerCase();
|
|
|
|
|
const existingContact = await tx.query.clientContacts.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(clientContacts.channel, 'email'),
|
|
|
|
|
sql`LOWER(${clientContacts.value}) = ${normalizedEmail}`,
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
if (existingContact) {
|
|
|
|
|
const existingClient = await tx.query.clients.findFirst({
|
|
|
|
|
where: eq(clients.id, existingContact.clientId),
|
|
|
|
|
});
|
|
|
|
|
if (existingClient && existingClient.portId === portId) {
|
|
|
|
|
clientId = existingClient.id;
|
|
|
|
|
const updates: Partial<typeof clients.$inferInsert> = {};
|
|
|
|
|
if (data.preferredContactMethod) {
|
|
|
|
|
updates.preferredContactMethod = data.preferredContactMethod;
|
|
|
|
|
}
|
|
|
|
|
if (data.nationalityIso && !existingClient.nationalityIso) {
|
|
|
|
|
updates.nationalityIso = data.nationalityIso;
|
|
|
|
|
}
|
|
|
|
|
if (Object.keys(updates).length > 0) {
|
|
|
|
|
await tx.update(clients).set(updates).where(eq(clients.id, clientId));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
clientId = await createClientInTx(tx, portId, fullName, data, phoneE164, phoneCountry);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
clientId = await createClientInTx(tx, portId, fullName, data, phoneE164, phoneCountry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Optional: upsert company + add membership
|
|
|
|
|
let companyId: string | null = null;
|
|
|
|
|
if (data.company) {
|
|
|
|
|
const existingCompany = await tx.query.companies.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(companies.portId, portId),
|
|
|
|
|
sql`lower(${companies.name}) = lower(${data.company.name})`,
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
if (existingCompany) {
|
|
|
|
|
companyId = existingCompany.id;
|
|
|
|
|
} else {
|
|
|
|
|
const [newCompany] = await tx
|
|
|
|
|
.insert(companies)
|
|
|
|
|
.values({
|
|
|
|
|
portId,
|
|
|
|
|
name: data.company.name,
|
|
|
|
|
legalName: data.company.legalName ?? null,
|
|
|
|
|
taxId: data.company.taxId ?? null,
|
|
|
|
|
incorporationCountryIso: data.company.incorporationCountryIso ?? null,
|
|
|
|
|
incorporationSubdivisionIso: data.company.incorporationSubdivisionIso ?? null,
|
|
|
|
|
status: 'active',
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
companyId = newCompany!.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add active membership only if one doesn't already exist (open row).
|
|
|
|
|
const existingMembership = await tx.query.companyMemberships.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(companyMemberships.companyId, companyId),
|
|
|
|
|
eq(companyMemberships.clientId, clientId),
|
|
|
|
|
isNull(companyMemberships.endDate),
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
if (!existingMembership) {
|
|
|
|
|
await tx.insert(companyMemberships).values({
|
|
|
|
|
companyId,
|
|
|
|
|
clientId,
|
|
|
|
|
role: data.company.role ?? 'representative',
|
|
|
|
|
startDate: new Date(),
|
|
|
|
|
isPrimary: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Create yacht. Owner is the company when provided, else the client.
|
|
|
|
|
const ownerType: 'client' | 'company' = companyId ? 'company' : 'client';
|
|
|
|
|
const ownerId = companyId ?? clientId;
|
|
|
|
|
const [newYacht] = await tx
|
|
|
|
|
.insert(yachts)
|
|
|
|
|
.values({
|
|
|
|
|
portId,
|
|
|
|
|
name: data.yacht.name,
|
|
|
|
|
hullNumber: data.yacht.hullNumber ?? null,
|
|
|
|
|
registration: data.yacht.registration ?? null,
|
|
|
|
|
flag: data.yacht.flag ?? null,
|
|
|
|
|
yearBuilt: data.yacht.yearBuilt ?? null,
|
|
|
|
|
lengthFt: data.yacht.lengthFt != null ? String(data.yacht.lengthFt) : null,
|
|
|
|
|
widthFt: data.yacht.widthFt != null ? String(data.yacht.widthFt) : null,
|
|
|
|
|
draftFt: data.yacht.draftFt != null ? String(data.yacht.draftFt) : null,
|
|
|
|
|
currentOwnerType: ownerType,
|
|
|
|
|
currentOwnerId: ownerId,
|
|
|
|
|
status: 'active',
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
const yachtId = newYacht!.id;
|
|
|
|
|
|
|
|
|
|
// 3a. Open ownership_history row for the new yacht.
|
|
|
|
|
await tx.insert(yachtOwnershipHistory).values({
|
|
|
|
|
yachtId,
|
|
|
|
|
ownerType,
|
|
|
|
|
ownerId,
|
|
|
|
|
startDate: new Date(),
|
|
|
|
|
endDate: null,
|
|
|
|
|
createdBy: 'public-submission',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 4. Store address if provided AND no primary address exists yet.
|
|
|
|
|
if (data.address && Object.values(data.address).some(Boolean)) {
|
|
|
|
|
const existingAddr = await tx.query.clientAddresses.findFirst({
|
|
|
|
|
where: and(eq(clientAddresses.clientId, clientId), eq(clientAddresses.isPrimary, true)),
|
|
|
|
|
});
|
|
|
|
|
if (!existingAddr) {
|
|
|
|
|
await tx.insert(clientAddresses).values({
|
|
|
|
|
clientId,
|
|
|
|
|
portId,
|
|
|
|
|
label: 'Primary',
|
|
|
|
|
streetAddress: data.address.street ?? null,
|
|
|
|
|
city: data.address.city ?? null,
|
|
|
|
|
subdivisionIso: data.address.subdivisionIso ?? null,
|
|
|
|
|
postalCode: data.address.postalCode ?? null,
|
|
|
|
|
countryIso: data.address.countryIso ?? null,
|
|
|
|
|
isPrimary: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Create interest with yachtId wired up.
|
|
|
|
|
const [newInterest] = await tx
|
|
|
|
|
.insert(interests)
|
|
|
|
|
.values({
|
|
|
|
|
portId,
|
|
|
|
|
clientId,
|
|
|
|
|
yachtId,
|
|
|
|
|
source: 'website',
|
feat(audit-session): legacy-stage canonicalization + multi-berth label sweep + PDF/UI polish
Critical data-correctness fixes
- external-eoi.service: stage-advance list rewritten against canonical
7-stage vocab (enquiry/qualified/nurturing → eoi). Was hardcoded to
legacy 9-stage names (open/details_sent/in_communication/eoi_sent), so
EOI uploads from 'qualified' silently skipped the stage flip. Now also
writes eoiDocStatus='signed' alongside eoiStatus='signed'.
- public-interest.service + api/public/interests/route: pipelineStage
'open' → 'enquiry' for new public interests.
- interests.service: legacy 'open' gate → 'enquiry'; inline-stage-picker
comments updated.
- Display fallbacks canonicalized: dashboard.service, dashboard-report-data,
pdf/templates/{interest,client}-summary, interest-picker, timeline route
all route through canonicalizeStage / stageLabelFor.
Multi-berth interest label sweep
- New helper src/lib/templates/interest-berth-label.ts with 9 unit tests
(deriveInterestBerthLabel reuses formatBerthRange + caps at 5 segments,
falls back to 'first + N more').
- New batched aggregator getAllBerthMooringsForInterests on the
interest-berths service.
- BoardInterestRow + listInterests + getInterest extended with
berthMoorings: string[].
- Swept render sites: interest-detail-header, pipeline-card +
pipeline-column (kanban), interest-columns (list), interest-card,
interest-detail (breadcrumb), client-pipeline-summary +
client-interests-tab, yacht-tabs, shared interest-picker.
- PDF report "New interests (in period)" Source column → Berth column.
Dashboard PDF report fixes
- Hardcoded EUR → reads ports.default_currency once at the top of
resolveDashboardReportData. Falls back to USD.
- 'maintenance' berth-status bucket removed everywhere (wasn't in
canonical BERTH_STATUSES); cleaned from dashboard.service,
dashboard-report-data, occupancy-report, berth-status-chart, fixture.
- Berth demand ranking: dropped placeholder Tier column (resolver
hardcoded 'A' — heat-tier never plumbed through).
- Deal pulse distribution: tier values capitalized (hot → Hot etc.).
- Validator widgetIds.max 20 → 40 (catalog has 25 entries; was throwing
"Validation failed" when all sections checked).
- Export dialog: badges tightened (text-[8px] py-px whitespace-nowrap, no
more 2-line wraps on "needs date range"); accepts initialRange?:
DateRange so the dashboard's active range pre-fills dateFrom/dateTo via
rangeToBounds.
Interest banner overcounts fix
- interest-berth-status-banner: filters out self-caused under-offer
berths (where the only active deal touching the berth IS this same
interest). Waits for all competing-queries before committing the
count. Was showing "3 berths unavailable" when only 1 actually had a
competitor.
Sessions list ordering
- sessions-list: client-side sort by lastAt desc + displays lastAt
instead of firstAt so visible timestamp matches the sort key.
Audit log polish
- Details button: side Sheet → Popover anchored to the button (in-place
inline dropdown). Works with the virtualized table.
- From/To date pickers: width w-44 → w-52, wrapper gap-3 → gap-x-4 gap-y-3.
EntityFolderView (Documents Hub entity view)
- Per-row Download button (hover-reveal icon).
- File-type icon prefix + tighter row layout.
- Per-row interest-berth badge: files.ts attaches interestBerthLabel via
one batched getAllBerthMooringsForInterests call across all groups.
AggregatedFile type + EntityFolderView render the badge linking back
to the parent interest.
External EOI upload dialog
- Title input pre-fills from the derived default via controlled
displayTitle = title || defaultTitle (no setState-in-effect).
EOI Generate dialog
- Success toast on mutation success.
- Primary berth's "Include in EOI" checkbox is now forced-on + disabled
with tooltip: the primary IS the canonical "berth for this deal",
excluding it is semantically nonsense.
Primary berth must always be in EOI bundle (service + backfill)
- interest-berths.service: insert path forces is_in_eoi_bundle=true
whenever is_primary=true; update path coerces back to true when the
caller tries to set false on a primary. Backfilled 7 existing rows.
Documenso redirect URL fallback
- port-config getPortDocumensoConfig: resolution chain extended to
documenso_redirect_url → public_site_url → null. Operators with
public_site_url configured (most ports) now get sensible signer
landing without setting two settings.
World-map click → navigate
- website-analytics-shell: country click navigates to the nationality-
filtered Clients page via router.push instead of copying a URL to
clipboard.
Documents Hub: subfolder grid in main panel
- Subfolder cards rendered above the documents list when the current
folder has children. Lets reps drill into subfolders from the main
content area, not only via the sidebar tree.
Interest list initial sort
- usePaginatedQuery gains initialSort option (used when URL has no sort
param). Interest list passes updatedAt desc so the table header
surfaces the active sort visibly + most-recently-added/edited bubble
to the top.
Interest auto-assign on create
- interests.service createInterest: three-tier owner resolution chain
— explicit input → port's default_new_interest_owner setting →
creator (when not super-admin). Super-admins skipped since they often
create on behalf of other reps.
Backfills
- 12 interests with eoi_status='signed' + missing eoi_doc_status='signed'
aligned.
- 7 interest_berths rows with is_primary=true but is_in_eoi_bundle=false
flipped to true.
Verified
- pnpm tsc --noEmit: clean
- pnpm exec vitest run: 1463 / 1463 passed
Captured 25+ additional UAT findings to docs/superpowers/audits/alpha-uat-master.md
across all 4 buckets, including two OPEN QUESTIONS (Reservations module
re-imagine, Reports dedicated page promotion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:41:27 +02:00
|
|
|
pipelineStage: 'enquiry',
|
feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
if (berthId) {
|
|
|
|
|
await tx.insert(interestBerths).values({
|
|
|
|
|
interestId: newInterest!.id,
|
|
|
|
|
berthId,
|
|
|
|
|
isPrimary: true,
|
|
|
|
|
isSpecificInterest: true,
|
|
|
|
|
isInEoiBundle: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
interestId: newInterest!.id,
|
|
|
|
|
clientId,
|
|
|
|
|
yachtId,
|
|
|
|
|
companyId,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...result,
|
|
|
|
|
berthId,
|
|
|
|
|
resolvedMooringNumber,
|
|
|
|
|
fullName,
|
|
|
|
|
firstName,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createClientInTx(
|
|
|
|
|
tx: Tx,
|
|
|
|
|
portId: string,
|
|
|
|
|
fullName: string,
|
|
|
|
|
data: Pick<PublicInterestData, 'email' | 'phone' | 'preferredContactMethod' | 'nationalityIso'>,
|
|
|
|
|
phoneE164: string | null,
|
|
|
|
|
phoneCountry: CountryCode | null,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const [newClient] = await tx
|
|
|
|
|
.insert(clients)
|
|
|
|
|
.values({
|
|
|
|
|
portId,
|
|
|
|
|
fullName,
|
|
|
|
|
preferredContactMethod: data.preferredContactMethod,
|
|
|
|
|
nationalityIso: data.nationalityIso ?? null,
|
|
|
|
|
source: 'website',
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
const clientId = newClient!.id;
|
|
|
|
|
|
|
|
|
|
await tx.insert(clientContacts).values({
|
|
|
|
|
clientId,
|
|
|
|
|
channel: 'email',
|
|
|
|
|
// Store lowercased so the case-insensitive dedup match above always
|
|
|
|
|
// hits on subsequent submissions.
|
|
|
|
|
value: data.email.trim().toLowerCase(),
|
|
|
|
|
isPrimary: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await tx.insert(clientContacts).values({
|
|
|
|
|
clientId,
|
|
|
|
|
channel: 'phone',
|
|
|
|
|
value: data.phone,
|
|
|
|
|
valueE164: phoneE164,
|
|
|
|
|
valueCountry: phoneCountry,
|
|
|
|
|
isPrimary: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return clientId;
|
|
|
|
|
}
|