295 lines
11 KiB
TypeScript
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 }),
|
|
};
|