feat(auth): split panel admins out of AdminUser into their own Admin table

AdminUser held both panel staff and sales experts, so one mobile could not be
both. Staff now live in "Admin" (panel only); sales experts stay in "AdminUser"
untouched. Uniqueness is per table.

- migration 20261026120000_split_admin_table: additive only. Creates "Admin",
  copies non-expert rows with their ids, re-points the "who did this" foreign
  keys (ledger, order status, sms, notification, upload, allocation, workshop,
  admin session) to "Admin" without moving data. Never writes to "AdminUser".
- login, JWT, SSO, branch/workshop supervisors and the users grid use "Admin";
  SALES_EXPERT create/edit still goes through admin/users into "AdminUser".
- an admin cannot be turned into an expert or the reverse (different tables).
- manual/: optional phase-2 script to drop the dormant admin copies from
  "AdminUser", and a read-only expert-login diagnostic.
- docs/ADMIN-TABLE-SPLIT.md: what changed, deploy checklist, rollback.

Tested on a copy with 12,000 experts: expert rows byte-identical (md5) before
and after; full migration chain applies and matches schema.prisma.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-25 23:52:33 +03:30
co-authored by Claude Sonnet 5.5
parent 4679e2498c
commit 06c8a5f434
21 changed files with 798 additions and 192 deletions
@@ -22,7 +22,7 @@ export interface PostEntryInput {
note?: string;
/** Order-delivery debt entries are self-confirming; repayments start pending. */
confirmed?: boolean;
postedByAdminUserId?: string;
postedByAdminId?: string;
}
/**
@@ -56,7 +56,7 @@ export class LedgerService {
note: input.note,
confirmed,
confirmedAt: confirmed ? new Date() : null,
postedByAdminUserId: input.postedByAdminUserId,
postedByAdminId: input.postedByAdminId,
},
});
@@ -83,7 +83,7 @@ export class LedgerService {
const updated = await tx.ledgerEntry.update({
where: { id: entryId },
data: { confirmed: true, confirmedAt: new Date(), postedByAdminUserId: adminUserId ?? entry.postedByAdminUserId },
data: { confirmed: true, confirmedAt: new Date(), postedByAdminId: adminUserId ?? entry.postedByAdminId },
});
await this.applyToBalance(entry.customerId, entry.currency, entry.direction, entry.amount, tx);
@@ -180,7 +180,7 @@ export class LedgerService {
karatPerMille?: number;
pricePerGramRial?: Prisma.Decimal.Value;
note?: string;
postedByAdminUserId?: string;
postedByAdminId?: string;
},
tx: Tx = this.prisma,
) {
@@ -230,7 +230,7 @@ export class LedgerService {
pricePerGramRialAtEntry: params.pricePerGramRial,
note: params.note,
confirmed: false, // pending until an admin confirms — intentional deviation from legacy, which auto-confirms on creation
postedByAdminUserId: params.postedByAdminUserId,
postedByAdminId: params.postedByAdminId,
},
tx,
);
@@ -242,7 +242,7 @@ export class LedgerService {
* migration analysis) but supported here since the new ledger is symmetric.
*/
async postRialRepayment(
params: { customerId: string; amount: Prisma.Decimal.Value; note?: string; postedByAdminUserId?: string },
params: { customerId: string; amount: Prisma.Decimal.Value; note?: string; postedByAdminId?: string },
tx: Tx = this.prisma,
) {
return this.postEntry(
@@ -254,7 +254,7 @@ export class LedgerService {
amount: params.amount,
note: params.note,
confirmed: false,
postedByAdminUserId: params.postedByAdminUserId,
postedByAdminId: params.postedByAdminId,
},
tx,
);
@@ -267,7 +267,7 @@ export class LedgerService {
direction: LedgerDirection;
amount: Prisma.Decimal.Value;
note: string;
postedByAdminUserId: string;
postedByAdminId: string;
},
tx: Tx = this.prisma,
) {
@@ -280,7 +280,7 @@ export class LedgerService {
amount: params.amount,
note: params.note,
confirmed: true,
postedByAdminUserId: params.postedByAdminUserId,
postedByAdminId: params.postedByAdminId,
},
tx,
);