/** 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(`${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(`${WEB}/tenants/search`, { status, keyword }), /** Branches under one tenant. `tenantid` is required — omit it and it 400s. */ locations: (tenantid: number) => api.list(`${WEB}/tenants/gettenantlocations`, { tenantid }), search: (keyword: string) => api.list(`${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(`${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(`${WEB}/tenants/createtenantlocation`, body), updateBranch: (body: Partial & { locationid: number }) => api.put(`${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) => api.put(`${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(`${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(`${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(`${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 { 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(`${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(`${WEB}/utils/getapplocations`, { applocationid }), };