Files
daily_console_web/src/api/tenants.ts
2026-09-09 15:43:23 +05:30

295 lines
11 KiB
TypeScript

/** Tenant and branch endpoints — the Nearle Admin's provisioning surface. */
import { api, WEB } from './client';
import type { AppLocation } from './deliveries';
import type { TenantInfo, TenantLocation } from './types';
/** Everything the tenant-onboarding form collects. */
export interface CreateTenantRequest {
tenantname: string;
companyname: string;
primarycontact: string;
primaryemail: string;
/**
* Who runs the shop — `tenants.firstname`.
*
* Asked here because here is the only moment it can be asked. The primary
* branch and the merchant's own admin login are both created inside
* `CreateTenantUser`'s transaction, and the login is copied from the tenant
* row — so a name given now names the business AND the account that will
* sign in. Left out, both are blank, which is what every merchant on the
* platform currently has: the store profile prints "—" for Store admin and
* the account carries no person's name at all.
*
* One field, not two: `tenants` has a `firstname` column and no
* `lastname`, so this is the whole name.
*/
firstname?: string;
locationname: string;
categoryid: number;
subcategoryid?: number;
address: string;
suburb?: string;
city: string;
state: string;
postcode: string;
latitude?: string;
longitude?: string;
moduleid?: number;
/** The city this merchant trades in. `app_location`, not a branch. */
applocationid?: number;
status?: string;
}
/** Everything the branch-onboarding form collects. */
export interface CreateBranchRequest {
tenantid: number;
locationname: string;
email?: string;
contactno?: string;
address: string;
suburb?: string;
city: string;
state: string;
postcode: string;
latitude?: string;
longitude?: string;
opentime?: string;
closetime?: string;
deliveryradius?: number;
deliverymins?: number;
status?: string;
/**
* Who will run this outlet — an existing person, when one has been hired
* already.
*
* Omitted, the backend spawns a login named after the SHOP, on the shop's
* email address, one per outlet. That was the only option, and it is why two
* people at a counter shared a credential and nothing recorded which of them
* did anything.
*
* A branch must still arrive with SOMEBODY: name a person here, or give an
* `email` to spawn one from. The backend refuses a branch with neither,
* because an outlet nobody can sign in to is a dead end that shows up in
* every list and is noticed by whoever is standing in the shop.
*/
operatorid?: number;
}
export interface TenantListQuery {
pageno?: number;
pagesize?: number;
/** `Active` / `InActive`. Omitted, the backend returns every state. */
status?: string;
applocationid?: number;
tenanttype?: string;
keyword?: string;
}
export const tenantsApi = {
/**
* Every tenant on the platform. Deliberately unscoped — this is the
* Nearle Admin's list, and the backend treats it as the platform-operator
* endpoint rather than a tenant-scoped one.
*/
listAll: (query: TenantListQuery = {}) =>
api.list<TenantInfo>(`${WEB}/tenants/getalltenants`, {
pageno: query.pageno ?? 1,
pagesize: query.pagesize ?? 100,
status: query.status,
applocationid: query.applocationid,
tenanttype: query.tenanttype,
keyword: query.keyword,
}),
/**
* Tenants by approval state — the only way to see the ones awaiting it.
*
* `status=pending` is not a status at all: the handler branches on the word
* and queries `approved = 0` instead (`tenantRepository.go:45-77`). Anything
* else means `approved = 1 AND status = ?`. So an unapproved merchant is
* invisible to every other endpoint, including `getalltenants`.
*
* Nothing can approve one over HTTP. `approved` is writable only at creation,
* so this list is a queue to work from, not one to act on.
*/
byApproval: (status: 'pending' | 'Active' | 'InActive', keyword?: string) =>
api.list<TenantInfo>(`${WEB}/tenants/search`, { status, keyword }),
/** Branches under one tenant. `tenantid` is required — omit it and it 400s. */
locations: (tenantid: number) =>
api.list<TenantLocation>(`${WEB}/tenants/gettenantlocations`, { tenantid }),
search: (keyword: string) =>
api.list<TenantInfo>(`${WEB}/tenants/searchbykeyword`, { keyword }),
/**
* Provisions the enterprise, its first outlet, and the primary Administrator
* account — one transaction writing `tenants`, `ordersequences`, `app_users`
* (roleid 1, configid forced to 1), `customers`, `customerlocations` and
* `tenantcustomers`.
*
* `createtenantuser`, NOT `createtenantlocation`. The latter takes a
* `Tenantlocations` and writes a BRANCH under a tenant that already exists —
* pointing the merchant form at it created an outlet and no merchant.
*
* The primary outlet is NESTED. The backend reads it off
* `Tenants.Tenantlocations` and creates it in the same transaction, so a
* tenant can never exist without somewhere to trade from.
*/
createTenant: (body: CreateTenantRequest) =>
api.post<TenantInfo>(`${WEB}/tenants/createtenantuser`, toTenantBody(body)),
/**
* Commissions a branch, and gives it somebody to run it.
*
* Pass `operatorid` to place a person you have already hired. Without it the
* backend spawns a login named after the shop, as it always did — kept so
* nothing existing changes, but the named person is the better path.
*
* `createtenantlocation`, not `createlocation`: only this one returns the
* created row, and the new `locationid` is what a QR code and every
* follow-up write need. `createlocation` answers 201 with a message and no
* `details` at all.
*/
createBranch: (body: CreateBranchRequest) =>
api.post<TenantLocation>(`${WEB}/tenants/createtenantlocation`, body),
updateBranch: (body: Partial<TenantLocation> & { locationid: number }) =>
api.put<TenantLocation>(`${WEB}/tenants/updatelocation`, body),
/**
* A merchant editing their own business record.
*
* The first write path `tenants` has ever had. Before it, everything about a
* shop — its name, its photograph, its licence, how to reach it — was set
* once at onboarding by a Nearle Admin and could never be changed by anyone.
*
* Only merchant-owned columns are written; the backend keeps the allowlist
* and ignores the rest, so `approved`, `status`, `partnerid` and the billing
* fields cannot be set from here even if a caller sends them. Anything
* omitted is left alone rather than blanked.
*/
updateProfile: (body: { tenantid: number } & Partial<TenantInfo>) =>
api.put<unknown>(`${WEB}/tenants/updatetenant`, body),
/**
* Which delivery partner supplies this merchant's riders.
*
* Its own endpoint, not a field on `updateProfile`: `partnerid` is kept out
* of the merchant-editable allowlist on purpose, because a merchant who could
* set it would move themselves under another partner's riders and billing.
*
* `partnerid: 0` is a real instruction — it means "this merchant uses their
* own riders" — and the server reads it as sent rather than as absent.
*/
assignPartner: (tenantid: number, partnerid: number) =>
api.put<unknown>(`${WEB}/tenants/assignpartner`, { tenantid, partnerid }),
/**
* One business, by id — how a store login reads its own record.
*
* Not `listAll`. That is `getalltenants`, paginated over 262 merchants, so a
* shop on page two was simply absent and a profile screen built on it would
* show nothing for no visible reason.
*/
byId: (tenantid: number) => api.get<TenantInfo>(`${WEB}/tenants/gettenantinfo`, { tenantid }),
/**
* Somebody editing their own name, mobile or email.
*
* Not `users/update`. That one writes whatever struct it is handed and checks
* only the userid — no tenant, no guard on role or branch — so a self-service
* form built on it would let a branch user promote themselves or move shop.
* This is scoped to the caller's own account AND business, and writes
* identity fields only.
*/
updateOwnProfile: (body: {
userid: number;
tenantid: number;
firstname?: string;
lastname?: string;
contactno?: string;
email?: string;
}) => api.put<unknown>(`${WEB}/tenants/updateownprofile`, body),
};
/**
* The merchant form, in the shape `models.Tenants` expects.
*
* `configid` and `applocationid` are sent because the account this call spawns
* is looked up by `configid` at every sign-in, and `applocationid` is the city
* the tenant trades in. `approved: 1` and `status: 'Active'` are set here
* because they can only ever be set here — there is no update or approve
* endpoint, so a tenant created unapproved stays unapproved forever.
*/
function toTenantBody(form: CreateTenantRequest): Record<string, unknown> {
return {
tenantname: form.tenantname,
companyname: form.companyname,
primaryemail: form.primaryemail,
primarycontact: form.primarycontact,
// Written to `tenants.firstname` and copied onto the admin login the same
// transaction creates — see the note on the field.
firstname: (form.firstname ?? '').trim(),
categoryid: form.categoryid,
subcategoryid: form.subcategoryid ?? 0,
address: form.address,
suburb: form.suburb ?? '',
city: form.city,
state: form.state,
postcode: form.postcode,
latitude: form.latitude ?? '',
longitude: form.longitude ?? '',
configid: 1,
moduleid: form.moduleid ?? 2,
applocationid: form.applocationid ?? 1,
approved: 1,
status: form.status ?? 'Active',
// The primary outlet, created in the same transaction. Its address
// defaults to the tenant's — a merchant's first shop is at the address
// they just typed far more often than not, and it can be edited after.
tenantlocations: {
locationname: form.locationname,
email: form.primaryemail,
contactno: form.primarycontact,
address: form.address,
suburb: form.suburb ?? '',
city: form.city,
state: form.state,
postcode: form.postcode,
latitude: form.latitude ?? '',
longitude: form.longitude ?? '',
applocationid: form.applocationid ?? 1,
status: 'Active',
},
};
}
/** One row of the `app_category` master — the business categories a tenant picks from. */
export interface AppCategory {
categoryid: number;
categoryname: string;
}
export const utilsApi = {
/**
* The business-category master.
*
* Read rather than hardcoded. The old console typed four values into the
* form and never called this, which means a category added to the master is
* invisible to onboarding until someone edits the frontend.
*/
appCategories: () => api.list<AppCategory>(`${WEB}/utils/getappcategories`),
/**
* The delivery regions — Coimbatore, Madurai, Nagercoil today.
*
* `applocationid` is REQUIRED by the handler and 0 is how you ask for all of
* them; omitting it answers 400 "Invalid applocationid", which reads as a
* broken request rather than a missing default.
*/
appLocations: (applocationid = 0) =>
api.list<AppLocation>(`${WEB}/utils/getapplocations`, { applocationid }),
};