Tenant Creation — Gap Analysis & Fix Plan
Reference BRD: docs/apps/manage/tenant-creation.md v1.3
Audited: April 20, 2026
Implemented: April 20, 2026
Scope: Admin portal (apps/manage) tenant creation form + API (apps/api/src/routers/admin/tenants.ts) + downstream integrations
Summary of Gaps
| # | Area | Gap | Priority | Status |
|---|---|---|---|---|
| 1 | Emails | Welcome email not sent on admin-created tenants | 🔴 High | ✅ Done |
| 2 | Emails | Login credentials email (with password) not implemented | 🔴 High | ✅ Done |
| 3 | Zoho | Zoho Books customer not auto-created on tenant creation | 🔴 High | ✅ Done |
| 4 | Zoho | Zoho Books invoice not auto-created after first invoice | 🔴 High | ✅ Done |
| 5 | Zoho | Draft invoice alert email (on Zoho failure) not implemented | 🔴 High | ✅ Done |
| 6 | Form | GSTIN format validation missing | 🟡 Medium | ✅ Done |
| 7 | Form | PAN number field does not exist (not collected, not stored) | 🟡 Medium | ✅ Done |
| 8 | Form | SEZ customer checkbox not in admin form | 🟡 Medium | ✅ Done |
| 9 | Tax | GST treatment not stored (gstTreatment field missing) | 🟡 Medium | ✅ Done |
| 10 | Tax | Tax type (IGST vs SGST+CGST) not computed or stored | 🟡 Medium | ✅ Done |
| 11 | Tax | Place of supply not stored | 🟡 Medium | ✅ Done |
| 12 | Tax | IGST/SGST/CGST amounts not stored on invoice | 🟡 Medium | ✅ Done |
| 13 | Invoice | Invoice status naming mismatch (pending vs draft/issued) | 🟢 Low | ✅ Done — Option A |
| 14 | Form | Password generator produces 16 chars (BRD: 8 chars) | 🟢 Low | ⚪ Kept at 16 (product decision) |
| 15 | Form | TDS percent is 10% in seeded Region data (BRD: 2%) | 🟢 Low | ✅ Done |
Gap 1 & 2 — Welcome Email + Credentials Email Not Sent
Problem
The welcome email (templateSlug: "welcome") is only enqueued inside apps/api/src/routers/auth.ts — the self-signup flow. When the Onboarding team creates a tenant via the admin portal (POST /admin/v1/tenants), no welcome email and no credentials email are sent to the customer.
The BRD requires two separate emails immediately after tenant creation:
- Welcome email —
"Welcome To Leadmetrics"— generic welcome, tells them to check inbox for credentials - Credentials email —
"Your Leadmetrics Credentials"— contains the username (email) and the temporary password
Fix
Step 1 — Create the credentials email template in the DB seed
File: packages/db/src/seed.ts
Add a new EmailTemplate record with slug: "credentials" (platform-level, tenantId: null). Template variables: firstName, lastName, email, password, dashboardUrl.
The welcome template (slug: "welcome") may already be seeded — verify. If not, add it too.
Step 2 — Enqueue both emails from the admin tenant creation route
File: apps/api/src/routers/admin/tenants.ts — inside the POST /tenants handler, after the audit log write and before the final reply.send.
Add two enqueueNotification calls:
// 1. Welcome email
enqueueNotification({
tenantId,
channel: "email",
type: "welcome",
templateSlug: "welcome",
recipients: [{ name: userName, email: body.email }],
variables: {
firstName: body.firstName.trim(),
lastName: body.lastName.trim(),
companyName: body.displayName.trim(),
dashboardUrl: process.env.DASHBOARD_URL ?? "https://app.leadmetrics.ai",
},
priority: "high",
dedupeKey: `welcome__${tenantId}`,
}).catch(err => fastify.log.error({ err }, "[create-tenant] Failed to enqueue welcome email"));
// 2. Credentials email
enqueueNotification({
tenantId,
channel: "email",
type: "credentials",
templateSlug: "credentials",
recipients: [{ name: userName, email: body.email }],
variables: {
firstName: body.firstName.trim(),
lastName: body.lastName.trim(),
email: body.email.toLowerCase().trim(),
password: body.password, // plain-text password before hashing is needed
dashboardUrl: process.env.DASHBOARD_URL ?? "https://app.leadmetrics.ai",
},
priority: "high",
dedupeKey: `credentials__${tenantId}`,
}).catch(err => fastify.log.error({ err }, "[create-tenant] Failed to enqueue credentials email"));Security note: The plain-text password must be captured before
hashPassword()is called. The current code hashes it early (const hashed = await hashPassword(body.password)). Store the raw value in aconst plainPassword = body.passwordbefore hashing, and passplainPasswordto the credentials email. The raw password is never stored — it exists only in memory for the duration of this request.
Step 3 — Add the credentials email template HTML
The notification server (apps/servers/notifications) loads email templates from the EmailTemplate DB table via email-loader.ts. The template content matches the HTML specified in BRD Section 14.2 — Email 2.
Gap 3, 4 & 5 — Zoho Books Auto-Sync Entirely Missing
Problem
ZohoBooksService (packages/providers/zoho/src/zoho-books.ts) is fully built — it has createCustomer(), createInvoice(), markInvoiceAsSent(), and all supporting methods. However, it is never called during tenant creation. Zoho Books is only wired as an optional manual channel connection.
The BRD requires:
- Auto-create Zoho customer profile on tenant creation
- Auto-create Zoho invoice after the first LM Portal invoice is created
- Fetch Zoho invoice number back → update LM Portal invoice status to
Issued - If Zoho sync fails → invoice stays in
Draft→ internal alert email sent to tech team
Fix
Step 1 — Store Zoho OAuth token as a system-level credential
The ZohoBooksService currently requires a per-user OAuth token (tied to the Zoho channel connection). For the auto-sync to work during tenant creation, the admin portal needs to use a system-level Zoho Books OAuth token — not a per-tenant one.
Options:
- Option A (recommended): Store the system Zoho OAuth token in environment variables (
ZOHO_BOOKS_CLIENT_ID,ZOHO_BOOKS_CLIENT_SECRET,ZOHO_BOOKS_REFRESH_TOKEN,ZOHO_BOOKS_ORG_ID). Create agetSystemZohoToken()helper that refreshes as needed. - Option B: Use the Admin user’s Zoho Books connected channel token (requires the admin to have connected Zoho Books in the manage portal).
Step 2 — Create a ZohoBooksSync service module
New file: apps/api/src/services/zoho-books-sync.ts
export async function syncTenantToZohoBooks(params: {
tenantId: string;
title: string; // Mr./Ms./etc
firstName: string;
lastName: string;
companyName: string; // legalName or displayName
displayName: string;
email: string;
phone: string;
billingName: string;
billingAddress: string;
billingCity: string;
billingState: string;
billingCountry: string;
billingPostal: string;
gstin: string | null;
panNumber: string | null;
tdsDeduction: boolean;
gstTreatment: string; // "Registered Business - Regular" | "Unregistered Business" | "SEZ" | "Overseas"
taxType: string | null; // "IGST" | "SGST+CGST" | null
taxRate: number;
placeOfSupply: string;
}): Promise<{ zohoCustomerId: string }>
export async function createZohoInvoice(params: {
zohoCustomerId: string;
invoiceId: string; // LM Portal invoice ID (for logging)
lineItems: { description: string; quantity: number; unitPrice: number }[];
currency: string;
dueDate: Date;
taxType: string | null;
taxRate: number;
tdsDeduction: boolean;
}): Promise<{ zohoInvoiceNumber: string }>Step 3 — Wire Zoho sync into the tenant creation route
File: apps/api/src/routers/admin/tenants.ts
After the invoice is created in the LM Portal DB, add:
// --- Zoho Books sync (best-effort, non-blocking) ---
(async () => {
try {
const { zohoCustomerId } = await syncTenantToZohoBooks({ ... });
const { zohoInvoiceNumber } = await createZohoInvoice({
zohoCustomerId,
invoiceId: invoice.id,
...
});
// Update invoice: status draft → issued, store Zoho invoice number
await db.invoice.update({
where: { id: invoice.id },
data: { status: "issued", externalInvoiceNumber: zohoInvoiceNumber },
});
} catch (err) {
fastify.log.error({ err, invoiceId: invoice.id }, "[create-tenant] Zoho sync failed");
// Send internal alert email
enqueueNotification({
tenantId,
channel: "email",
type: "draft_invoice_alert",
templateSlug: "draft_invoice_alert",
recipients: [{ name: "Tech Team", email: process.env.TECH_TEAM_EMAIL! }],
variables: {
tenantName: tenant.name,
invoiceId: invoice.id,
billedOn: now.toISOString(),
createdBy: actor?.name ?? "Super Admin",
errorMessage: String(err),
},
priority: "high",
}).catch(() => {});
}
})();The Zoho sync runs asynchronously after the response is sent so the admin sees a success toast regardless of Zoho outcome (matching BRD Section 7 — Alternative Path A).
Step 4 — Add DB fields for Zoho sync state
Add to Subscription model (or a new TenantBillingProfile model):
zohoCustomerId String?— Zoho Books contact IDzohoSyncedAt DateTime?— last successful sync time
Add to Invoice model:
externalInvoiceNumber String?— Zoho invoice number (currentlyinvoiceNumberrefers to the internal LM PortalINV-YYYY-MM-NNNNnumber)
Step 5 — Create draft_invoice_alert email template
Seed a new EmailTemplate with slug: "draft_invoice_alert", tenantId: null using the HTML from BRD Section 11.3. Template variables: tenantName, invoiceId, billedOn, createdBy, errorMessage.
Gap 6 — GSTIN Format Validation Missing
Problem
The form accepts any string in the GSTIN field. The BRD requires format validation matching the Indian GST number pattern: ^\d{2}[A-Z]{5}\d{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$ (15 characters).
Fix
In the form — CreateTenantForm.tsx
Add client-side validation in the submit handler:
if (hasGST && gstin) {
const gstinPattern = /^\d{2}[A-Z]{5}\d{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/;
if (!gstinPattern.test(gstin.toUpperCase())) {
setErrors(e => ({ ...e, gstin: 'Invalid GST number format. Must be 15 characters (e.g., 29ABCDE1234F1Z5)' }));
return;
}
}Also auto-uppercase the field value on input change.
In the API — apps/api/src/routers/admin/tenants.ts
Add server-side validation:
if (body.gstin) {
const gstinPattern = /^\d{2}[A-Z]{5}\d{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/;
if (!gstinPattern.test(body.gstin.toUpperCase())) {
return apiError(reply, 400, "VALIDATION_ERROR", "Invalid GSTIN format.");
}
}Gap 7 — PAN Number Not Collected or Stored
Problem
PAN number is required for Indian GST compliance and syncing to Zoho Books. The field does not exist in the form, the API body, or the database schema.
Fix
Step 1 — DB schema: add panNumber to Subscription
File: packages/db/prisma/schema.prisma
// inside the Subscription model, near billingGstin
billingPanNumber String? // PAN number (India only)Run pnpm db:migrate.
Step 2 — API: accept panNumber in the body
File: apps/api/src/routers/admin/tenants.ts
Add to the body schema:
panNumber: { type: "string", description: "PAN number for India tenants (optional)" }Add PAN validation:
if (body.panNumber) {
const panPattern = /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/;
if (!panPattern.test(body.panNumber.toUpperCase())) {
return apiError(reply, 400, "VALIDATION_ERROR", "Invalid PAN number format.");
}
}Store on subscription create:
billingPanNumber: body.panNumber?.toUpperCase() || null,Step 3 — Form: add PAN input field
File: apps/manage/src/app/(manage)/tenants/create/CreateTenantForm.tsx
In Section 2 (Company Information), below the Display Name field, add:
{/* PAN Number — India only */}
{country === "IN" && (
<div>
<label className={labelCls}>PAN Number <span style={{ color: "#9ca3af" }}>(Optional)</span></label>
<input
className={inputCls}
style={inputStyle}
placeholder="e.g. ABCDE1234F"
value={panNumber}
onChange={e => setPanNumber(e.target.value.toUpperCase())}
maxLength={10}
/>
{errors.panNumber && <p className={errCls} style={errStyle}>{errors.panNumber}</p>}
</div>
)}Add PAN format validation in the submit handler (same pattern as above).
Gap 8 — SEZ Customer Checkbox Not in Admin Form
Problem
The billingSezStatus field exists on the Subscription model and is populated from the self-signup flow, but the admin form has no SEZ checkbox. The BRD specifies the checkbox appears when a valid GSTIN is entered.
Fix
Form: add SEZ checkbox
File: apps/manage/src/app/(manage)/tenants/create/CreateTenantForm.tsx
In Section 1 (Location & Tax Details), show the checkbox when hasGST === "yes" and gstin is non-empty:
{hasGST === "yes" && gstin && (
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
checked={isSez}
onChange={e => setIsSez(e.target.checked)}
className="mt-0.5"
/>
<span className="text-xs text-slate-300">
This is a SEZ (Special Economic Zone) customer
<span className="block text-xs text-slate-500 mt-0.5">
SEZ customers are exempt from GST. Select this only if the customer is registered under a Special Economic Zone.
</span>
</span>
</label>
)}API: accept and store sezCustomer
File: apps/api/src/routers/admin/tenants.ts
Add to body schema:
sezCustomer: { type: "boolean" }Store on subscription create:
billingSezStatus: body.sezCustomer ? "yes" : "no",Gap 9, 10 & 11 — GST Treatment, Tax Type, Place of Supply Not Stored
Problem
The tax summary card in the form is a display-only UI widget — the computed values (gstTreatment, taxType, placeOfSupply) are never sent to the API and never stored. This means Zoho Books cannot be correctly configured and audit queries cannot be run on tax treatment history.
Fix
Step 1 — DB schema: add tax fields to Subscription
File: packages/db/prisma/schema.prisma
// inside Subscription model, near billingGstin
gstTreatment String? // "Registered Business - Regular" | "Unregistered Business" | "SEZ" | "Overseas"
taxType String? // "IGST" | "SGST+CGST" | null (for SEZ/Overseas)
placeOfSupply String? // Indian state name or "Foreign Country"Run pnpm db:migrate.
Step 2 — Compute these values server-side in the API
File: apps/api/src/routers/admin/tenants.ts
Extract a pure function (can live in apps/api/src/lib/tax.ts):
export function computeTaxSettings(params: {
country: string;
state: string | null;
hasGstin: boolean;
isSez: boolean;
}): {
gstTreatment: string;
taxType: string | null;
taxRate: number;
placeOfSupply: string;
} {
if (params.country !== "IN") {
return { gstTreatment: "Overseas", taxType: null, taxRate: 0, placeOfSupply: "Foreign Country" };
}
if (params.hasGstin && params.isSez) {
return { gstTreatment: "SEZ", taxType: null, taxRate: 0, placeOfSupply: params.state ?? "" };
}
const treatment = params.hasGstin ? "Registered Business - Regular" : "Unregistered Business";
const isKerala = params.state === "Kerala";
return {
gstTreatment: treatment,
taxType: isKerala ? "SGST+CGST" : "IGST",
taxRate: 18,
placeOfSupply: params.state ?? "",
};
}Call this in the tenant creation handler and store the result on the subscription.
Step 3 — Add state to the API body
File: apps/api/src/routers/admin/tenants.ts
The state field (Indian state for tax purposes) is currently not included in the API body even though the form collects it. Add:
state: { type: "string", description: "Indian state (for GST tax type calculation)" }Store it: billingState currently holds the billing address state; the tax state is separate. Consider:
- Add a dedicated
taxState String?field onSubscription, or - Accept
taxStateas a separate body field alongsidebillingState
Step 4 — Update the form to send the new fields
File: apps/manage/src/app/(manage)/tenants/create/actions.ts
Add state, sezCustomer, panNumber, gstTreatment, taxType, placeOfSupply to the payload sent to POST /admin/v1/tenants.
Gap 12 — IGST/SGST/CGST Amounts Not Stored on Invoice
Problem
The Invoice model stores a single taxAmount with a flat taxPercent. Indian GST compliance requires the invoice to break down the tax as either:
- IGST: single 18% line
- SGST + CGST: two 9% lines each
This breakdown needs to be stored on the invoice and displayed on invoice PDFs.
Fix
DB schema: add GST component fields to Invoice
File: packages/db/prisma/schema.prisma
// inside Invoice model, near taxAmount
igstAmount Int? // in smallest currency unit; set when taxType = "IGST"
sgstAmount Int? // in smallest currency unit; set when taxType = "SGST+CGST"
cgstAmount Int? // in smallest currency unit; set when taxType = "SGST+CGST"
taxType String? // "IGST" | "SGST+CGST" | null
placeOfSupply String?
gstTreatment String?API: populate these fields when creating the invoice
File: apps/api/src/routers/admin/tenants.ts
const taxSettings = computeTaxSettings({ country, state, hasGstin, isSez });
const igstAmount = taxSettings.taxType === "IGST" ? taxAmount : null;
const sgstAmount = taxSettings.taxType === "SGST+CGST" ? taxAmount / 2 : null;
const cgstAmount = taxSettings.taxType === "SGST+CGST" ? taxAmount / 2 : null;
await db.invoice.create({
data: {
...
taxType: taxSettings.taxType,
placeOfSupply: taxSettings.placeOfSupply,
gstTreatment: taxSettings.gstTreatment,
igstAmount,
sgstAmount,
cgstAmount,
},
});Gap 13 — Invoice Status Naming Mismatch
Problem
The BRD uses Draft and Issued as invoice statuses. The code uses pending. There is also no draft state in the current status flow — the Zoho sync happens asynchronously after a pending invoice is already created.
Recommendation
Two options:
Option A — Align code to BRD (recommended):
- Rename
pending→draftwhen invoice is first created (before Zoho sync) - After successful Zoho sync → update to
issued - This requires adding a DB migration to rename the enum value and updating all references
Option B — Update BRD to reflect code:
- Document that the actual states are
pending(pre-payment) and the BRD’s “Issued” maps to the samependingstate since the invoice is already visible to customers - Simpler but maintains a terminology gap between BRD and code
Gap 14 — Password Generator Length
Problem
The form’s generatePassword() function produces a 16-character password. The BRD specifies 8 characters (minimum recommended).
Fix
File: apps/manage/src/app/(manage)/tenants/create/CreateTenantForm.tsx
// Change length from 16 to 8
return Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");Note: 16 characters is more secure. Confirm with product whether BRD’s “8 characters” is a minimum (in which case 16 is fine) or an exact length requirement.
Gap 15 — TDS Percentage Discrepancy
Problem
The BRD states TDS is fixed at 2%. The Region table seeds tdsPercent for India as 10%.
Fix
If BRD is correct (2% for SaaS services under section 194J): Update the India region seed in packages/db/src/seed.ts to tdsPercent: 2. Run pnpm db:seed on staging to update.
If 10% is correct (10% for professional services): Update the BRD and document the applicable TDS section.
Action required: Confirm the correct TDS section (194J at 2% vs 194J at 10%) with the finance/tax team before making changes.
Implementation Task Plan
Phase 1 — Emails ✅ Complete
| Task | File | Status |
|---|---|---|
1.1 Seed welcome and credentials email templates | packages/db/src/seed.ts | ✅ |
1.2 Seed draft_invoice_alert internal email template | packages/db/src/seed.ts | ✅ |
| 1.3 Enqueue welcome email on admin tenant creation | apps/api/src/routers/admin/tenants.ts | ✅ |
| 1.4 Enqueue credentials email on admin tenant creation | apps/api/src/routers/admin/tenants.ts | ✅ |
| 1.5 Capture plain password before hashing for credentials email | apps/api/src/routers/admin/tenants.ts | ✅ |
Phase 2 — Form Validation Fixes ✅ Complete
| Task | File | Status |
|---|---|---|
| 2.1 Add GSTIN format validation (client + server) | CreateTenantForm.tsx + admin/tenants.ts | ✅ |
| 2.2 Auto-uppercase GSTIN field | CreateTenantForm.tsx | ✅ |
| 2.3 Confirm and fix password generator length (8 vs 16) | CreateTenantForm.tsx | ⚪ Kept at 16 (product decision) |
Phase 3 — Tax Fields ✅ Complete
| Task | File | Status |
|---|---|---|
3.1 Add billingPanNumber, gstTreatment, taxType, placeOfSupply to Subscription model | schema.prisma | ✅ |
3.2 Add igstAmount, sgstAmount, cgstAmount, taxType, placeOfSupply, gstTreatment to Invoice model | schema.prisma | ✅ |
| 3.3 Run DB migration | 20260420_add_tax_zoho_fields | ✅ |
3.4 Create computeTaxSettings() helper | apps/api/src/lib/tax.ts | ✅ |
| 3.5 Add PAN field to form (India-only, with format validation) | CreateTenantForm.tsx | ✅ |
| 3.6 Add SEZ checkbox to form (shown when GSTIN entered) | CreateTenantForm.tsx | ✅ |
| 3.7 Update API to accept + store all new tax fields | apps/api/src/routers/admin/tenants.ts | ✅ |
3.8 Update actions.ts to send all new fields in the POST body | apps/manage/.../tenants/create/actions.ts | ✅ |
| 3.9 Fix TDS % to 2% | packages/db/src/seed.ts | ✅ |
Phase 4 — Zoho Books Auto-Sync ✅ Complete (env vars pending)
| Task | File | Status |
|---|---|---|
| 4.1 System Zoho OAuth token via env vars (Option A) | Architecture decision | ✅ |
| 4.2 System-level token refresh with in-process cache | apps/api/src/services/zoho-books-sync.ts | ✅ |
4.3 Create zoho-books-sync.ts service module | apps/api/src/services/zoho-books-sync.ts | ✅ |
4.4 Add zohoCustomerId, zohoSyncedAt to Subscription model | schema.prisma | ✅ |
4.5 Add externalInvoiceNumber to Invoice model | schema.prisma | ✅ |
| 4.6 Run DB migration | 20260420_add_tax_zoho_fields | ✅ |
| 4.7 Wire Zoho sync (async, best-effort) into tenant creation route | apps/api/src/routers/admin/tenants.ts | ✅ |
| 4.8 Implement draft invoice alert email on Zoho failure | apps/api/src/routers/admin/tenants.ts | ✅ |
| 4.9 Handle “Zoho profile already exists” edge case | apps/api/src/services/zoho-books-sync.ts | ⏳ Deferred |
4.10 Invoice status rename (pending → draft/issued) | Multiple files | ✅ Option A |
Effort Key
| Symbol | Meaning |
|---|---|
| XS | < 30 minutes |
| S | 30 min – 2 hours |
| M | Half a day |
| L | Full day |
Implementation Notes (April 20, 2026)
Key decisions made during implementation
- Gap 14 (password length): Kept at 16 characters. 16 is more secure than 8; BRD value treated as minimum not exact.
- Gap 13 (invoice status): Option A chosen.
pending→drafton admin creation;issuedafter Zoho sync. - Tax state: Reused
billingStatefor GST tax type computation (body.billingState || body.state). No separatetaxStatefield needed. - Zoho auth: Option A — system-level env vars (
ZOHO_BOOKS_CLIENT_ID,ZOHO_BOOKS_CLIENT_SECRET,ZOHO_BOOKS_REFRESH_TOKEN,ZOHO_BOOKS_ORG_ID,ZOHO_BOOKS_ACCOUNTS_SERVER). Add toapps/api/.envbefore going live. - Alert email recipient: New
TECH_TEAM_EMAILenv var. Default:moble@leadmetrics.ai.
Files changed
| File | Change |
|---|---|
packages/db/prisma/schema.prisma | Added 13 new fields on Subscription + Invoice models |
packages/db/prisma/migrations/20260420_add_tax_zoho_fields/ | New migration for all tax + Zoho fields |
packages/db/src/seed.ts | TDS 10→2%; added credentials + draft_invoice_alert email templates |
packages/common/src/invoice.ts | Added "issued" to VALID_INVOICE_STATUSES |
apps/api/src/lib/tax.ts | NEW — computeTaxSettings() pure function |
apps/api/src/services/zoho-books-sync.ts | NEW — syncTenantToZohoBooks() + createZohoInvoice() |
apps/api/src/routers/admin/tenants.ts | Full overhaul: GSTIN/PAN validation, emails, tax fields, Zoho sync, draft status |
apps/api/src/routers/admin/billing.ts | Added "issued" to status enums; fixed listTenantInvoices pagination schema |
apps/manage/.../tenants/create/CreateTenantForm.tsx | Added PAN input, SEZ checkbox, GSTIN format validation |
apps/manage/.../tenants/create/actions.ts | Added panNumber, sezCustomer to CreateTenantInput |
apps/manage/.../invoices/actions.ts | Manual invoice status "pending" → "draft" |
apps/manage/tests/e2e/global-setup.ts | Fixed tdsPercent: 10 → tdsPercent: 2 |
Test fixes applied
| File | Fix |
|---|---|
packages/common/src/__tests__/invoice.test.ts | Updated length assertion 5→6; added "issued" to it.each |
apps/api/src/routers/admin/billing.ts | Fixed listTenantInvoices response schema to include page, limit, totalPages |
leadmetrics_api_test DB | Dropped and recreated with migrate deploy + db push + seed |
leadmetrics_manage_test DB | Created new DB with migrate deploy + db push + seed |
Outstanding (not implemented)
- Gap 4.9: “Zoho profile already exists” edge case — if the same tenant creation is retried, a duplicate Zoho customer may be created. Low risk in normal usage.
- Zoho env vars:
ZOHO_BOOKS_*vars added toapps/api/.envas blank strings. Must be filled with real credentials before testing Zoho sync in staging/production.