OctoWiki

Client Configuration

Every field on the client (tenant) record and what it actually controls, plus the per-client config tables — discounts, payout limits, IP whitelist, portal users, blacklists — and the create-client flow. Includes the dead fields and default mismatches.

The clients table / models.Client (database/models/client.go:17) is the tenant record. This traces every field to its real read sites — several are set-but-never-read — plus the per-client config tables that hang off it.

The live struct is much leaner than the table's historyis_active, is_bulk_limit_enabled, is_fill_or_kill_enabled, show_all_products and others were added then dropped or orphaned. See Dead fields & mismatches.

models.Client fields

Field / columnTypeWhat it controls (traced)DefaultGotchas
ID / iduint64 PKTenant identity; types.TenantIDKey drives all tenant-scoped queriesserialStoreClientWithID can force an ID for seeding
Name / namestringDisplay + search only; GetClientByName case-insensitiverequired≥3 chars on create; no uniqueness constraint
Timezone / timezonestringStored; dashboard/reporting'UTC'required non-empty; not validated against a tz DB
Currency / currencystring (FK)Base/reporting currency — provisions the initial wallet + ReportingClientCurrency on orders. NOT the order-time wallet key (see below)requiredmust exist, exactly 3 chars
Status / statusClientStatusThe single access gateIsActive() = Status==ACTIVE, checked in API + portal auth'ACTIVE'enum INVITED/ACTIVE/SUSPENDED/DELETED; only ACTIVE logs in
BulkLimit / bulk_limitintMax order quantity — enforced at every order entry100required >0 on create; blank edit silently sets 0 (blocks all orders)
IsLinkEnabled / is_link_enabledboolWhether the client may place GV-Link (claimable) orderscolumn was originally is_gv_link_enabled, renamed; not editable via the admin edit form
ShopifyEnabled / shopify_enabledboolMaster Shopify toggle; gates credential probe + GetShopifyEnabledClientsFALSErequires shop name + token when on
ShopifyShopNamestring*.myshopify.com host for the API client''must contain myshopify.com
ShopifyAccessTokenstring (encrypted)Admin API token, AES-GCM at rest''blank on edit keeps existing token
ShopifyAPIVersionstringShopify API versionDB '2025-01' / code '2026-04'default mismatch (see below)
ShopifyLastSyncAt / …CancelSyncAt*time.TimeSeparate poller watermarks (order sync vs cancel sync)NULLkept distinct so pollers don't trample
ShopifySyncEnabledboolAuto-sync toggle; both shopify_enabled AND shopify_sync_enabled neededDB TRUEcreate takes it from an unchecked form box → false unless ticked
ParentID / HierarchyLevel*uint64 / intStored, but no behavioral read site — hierarchy actually runs through sub_clientsNULL / 1edit can silently zero HierarchyLevel
ImageURL*stringClient logoNULLno create/edit field wires it — effectively always NULL
CreatedAt / UpdatedAt / DeletedAttimeAudit + soft-delete (every read filters deleted_at IS NULL)

BulkLimit — where the cap bites

Enforced at every order entry: charges preview (charges.go:82), voucher create (create_voucher_order.go:569), portal product order (client_products.go:402), and as the ceiling in utils.GetMaxQuantity. Non-link orders are additionally clamped to a hard-coded 5000 (utils/charges.go:176), so effective cap = min(BulkLimit, 5000); link orders can exceed 5000 up to BulkLimit.

Currency — reporting, not the runtime wallet

Client.Currency provisions the initial Prepaid wallet at create and is recorded as ReportingClientCurrency on orders. But at order time the wallet is resolved against the product's currency, not the client's, with RequireCurrencyMatch=false (create_voucher_order.go:675) — so a client can transact against wallets in other currencies, with FX applied. See Wallet & Ledger → resolution.

Status / activation — what deactivation blocks

The old is_active bool was dropped (20260710000001); active-state derives solely from Status. AdminClientToggleStatusHandler flips ACTIVESUSPENDED (refuses DELETED) and, on deactivation, revokes all client tokens for an instant cutover. A non-ACTIVE client is blocked from API JWT auth (middleware/auth.go:93 → 403) and portal cookie auth (client_cookie_auth.go:98, a whole-tenant gate even if the individual ClientUser is active). INVITED is also treated as inactive. See Auth & Access.

The discount default divergence, resolved

There's no discount field on Client — defaults are computed. Two "default" constants exist; the live one is −1%:

  • Live path (listing and checkout): repo.getDiscountForVendorProduct uses DEFAULT_DISCOUNT = 1.0discount = vendor_discount − 1% when no CPD row exists.
  • Dead path: utils.GetDiscountForVendorProduct returns a −4.0 surcharge — defined and unit-tested but never called in production.

So the create-handler success note claiming "−4% surcharge during checkout" (admin_client.go:376) is stale/inaccurate — checkout actually applies −1%. See Known Issues.

No rate/credit/postpaid/prepaid fields live on Client — prepaid/postpaid is a wallet property (models.Wallet.Type), and create always provisions a Prepaid wallet.

Per-client config tables

Everything configurable per tenant lives in dedicated tables, not on the client row:

TablePurposeKey columns / notes
client_product_details (CPD)Per-(client, vendor_product) discount overridediscount, is_active, link_type; present → overrides the −1% default. See Products & Catalog
client_payout_configsPayout limits + enablepayouts_enabled, daily/monthly/min/max, allowed currencies/providers (JSONB); ⚠️ auto_queue_payouts + require_beneficiary_verification are columns with no Go fields. See Payouts
ip_whitelist_entriesPer-client IP allow-listcidr, is_active; empty = allow-all, fails open on DB error. See Auth
client_credentialsMachine API keysusername/hashed password, name, is_active; unique (client_id, name)
client_usersHuman portal logins + rolesemail (globally unique), role (owner/admin/write/read), status, 2FA cols. Permissions in client_user.go:50
client_product_blacklistsHide products from a clientproduct_id, is_active
client_vendor_blacklistsHide a vendor's productsvendor_id, is_active
client_product_configsPer-(client, product) link/prefetch togglesis_link_blacklisted, is_prefetch_blacklisted
client_topup_products / client_esim_variantsPer-client top-up/eSIM discountsdiscount, is_active; no row → discount 0, all active accessible
client_payout_webhooksPayout event webhooksurl, secret, events (JSONB), failure tracking
sub_clientsThe real hierarchy mechanism (vs the inert Client.ParentID)client_id, parent_id

client_credentials (API keys) and client_users (portal logins) overlap post-migration 20251209181115, which migrated old credential rows into client_users as owner. Both coexist: credentials = machine keys, client_users = humans. Related: client_backup_code (2FA recovery), client_password_reset_token (single-use SHA-256 tokens).

Creating a client

AdminCreateClientHandler (admin_client.go:137), single tx:

Not created at signup: no CPD rows, no payout config, no IP whitelist, no API client_credentials key, no blacklists. The success note tells the admin to configure product discounts manually or the client sees vendor_discount − 1%.

Update (AdminClientUpdateDetailsHandler) can change name, timezone, currency, hierarchy_level, bulk_limit, and Shopify fields — but not status, is_link_enabled, or image_url. Blank bulk_limit/hierarchy_level default to 0 here (the silent-zero gotcha). Activation is a separate handler.

Dead fields & default mismatches

ItemStatus
show_all_products (column)Completely dead — not on the struct, never read/written; the whitelist-via-blacklist toggle was never wired
Client.ParentID / HierarchyLevelStored, no behavioral read; hierarchy runs through sub_clients; edit can zero the level
is_active, is_bulk_limit_enabled, is_fill_or_kill_enabledAdded then dropped
is_gv_link_enabledOriginal column name, renamed to is_link_enabled — watch when reading old migrations
Shopify API versionDB default '2025-01' vs code '2026-04' — raw-SQL inserts get 2025-01 (see Shopify)
ShopifySyncEnabledDB default TRUE, but create sets it from an unchecked box → false unless ticked
−4.0 discount surchargeDead code; live path is −1% (above)
client_payout_configs.auto_queue_payouts / require_beneficiary_verificationColumns with no Go fields — set-but-never-read
Client.ImageURLNo form field wires it — always NULL

Key files

  • Model/repo: database/models/client.go, database/repo/client.go
  • Create/update: http/handler/admin_client.go, http/handler/admin_ui.go (toggle :1554, update :1401)
  • Enforcement: middleware/auth.go, middleware/client_cookie_auth.go, middleware/ip_whitelist.go, utils/charges.go
  • Config tables: database/models/client_*.go, database/models/{ip_whitelist,auth_token,client_user}.go

On this page