Chuyển tới nội dung chính

Banners

In one line: the list of promo banners on the client app's home screen, and what each one does when tapped.

When you'd use this​

Marketing creates banners in the operator tool — "20% off this weekend," "New: Burger Bros," "Try the closest branch." This endpoint gives the app the banners that are live right now, in the order they should be shown. Each banner also says what a tap should do: open a link, open a specific store, or find the customer's nearest branch of a brand.

Call it when the home screen (or a service's screen, like Groceries) loads.

Example: rendering the home screen​

Ids and names below are illustrative.

  1. The app opens the home screen and calls GET /api/v2/client/banners.

  2. Three banners come back, highest sequence first:

    Bannerlink_typeWhat the tap does
    "Weekend deals"urlOpens https://…/weekend from the banner's link.
    "Burger Bros JBR"storeOpens store 1874 (from the banner's merchant_id field — see Watch out).
    "Order Burger Bros"nearest_branchCalls Nearest store with nearest_branch_merchant_id = 42 and opens whichever branch it returns.
  3. The customer switches to the Groceries tab. The app calls the same endpoint again with that tab's service_id to get the Groceries banners.

A banner that is still a draft, has expired, or hasn't started yet never shows up — the app doesn't need to filter anything.

How to call it​

GET /api/v2/client/banners

Endpoint: GET /api/v2/client/banners — OpenAPI operation ClientBannersController_getBannersList in openapi/order-v2-client.json, where the exact request and response fields live.

  1. For banners not tied to any service, call it without service_id. For a service's own banners, pass that service's service_id.
  2. Render in the order returned.
  3. On tap, switch on link_type (table above).

Watch out​

  • No service_id ≠ all banners. It means "only banners that aren't tied to a service."
  • A banner's merchant_id is actually a store id. For nearest_branch banners, use nearest_branch_merchant_id instead.
  • An empty list might be a server error. Errors come back as 200 with zero banners. If the home screen is unexpectedly empty, check the server logs.

Details (for backend)​

Everything below is the reasoning and the exact rules — read it when you're changing this endpoint or debugging a surprising result.

Module: src/modules/client/banners/. Backed by src/modules/core/banners/banners.service.ts and the AhsanBanner entity (src/modules/locateMerchantV2/entities/banner/banner.entity.ts), reading the same banners table the operator API in locate-merchant-v2-api writes to.

What comes back, and why​

  • Only published banners in their active window are returned. A banner needs status = 'publish', and "now" has to fall between starts_at and expires_at — a null starts_at counts as already started, a null expires_at counts as never expiring, and both bounds are inclusive. Draft banners and expired/not-yet-started ones never reach the client. (src/modules/core/banners/banners.service.ts, processQueryBuilder.)
  • Omitting service_id does not mean "all banners." Pass service_id and you get banners scoped to that system category; leave it out and the query filters to banners whose service_id is null — the unscoped ones. There's no query that returns every banner regardless of scope in one call. (Same file — if (service_id) {...} else { queryBuilder.andWhere({ service_id: IsNull() }) }.)
  • Ordering is by sequence, descending. Not by starts_at, not by id.
  • latitude/longitude are accepted on the request but not used. QueryBannerDto declares them (src/modules/core/banners/dto/query-banner.dto.ts), but processQueryBuilder never reads them and they play no part in the Redis cache key either — sending them changes nothing about the response. Not documented as intentional; treat it as dead parameters unless someone confirms otherwise.
  • Caching is narrow. Only the exact request ?assigned=unassigned&language=<lang> is served from Redis (30s TTL, key built in src/shared/helper/client-read-api-cache.helper.ts, getClientBannersCacheKey). Any other query — including every service_id-scoped request — always hits the database.
  • A database error looks like "no banners," not an error. ClientBannersService.getBanners catches everything and falls back to an empty, zero-item paginated payload with a 200. If the home screen is unexpectedly empty, check the server logs before assuming there are genuinely no active banners — this also covers the case where a required column (e.g. link_type, target_merchant_id) hasn't been migrated into an environment yet; see docs/sql/banners-link-type-target-merchant.sql in locate-order-v2-api for that deploy-order hazard.

Every banner has a link_type, defaulting to url for all pre-existing banners:

link_typeBehavior
urlOpen the banner's link field verbatim.
storeOpen the store in the banner's merchant_id field.
nearest_branchCall Nearest store with the banner's nearest_branch_merchant_id and the user's coordinates; open the store_id it returns, or do nothing if it's null.

The merchant_id field on a banner is not a merchant id. It's a getter over the banner's store_id column (get merchant_id() { return this.store_id || null; } in banner.entity.ts), kept under its old name for backward compatibility. The one field that is a real merchants.id is nearest_branch_merchant_id, and it's only ever set when link_type is nearest_branch. Passing the wrong one into Nearest store's :merchantId doesn't error — it comes back 200 with a null store_id, which looks exactly like "no branch nearby." Full hazard table on the Nearest store page.

Not documented yet​

  • Whether latitude/longitude were meant to drive some future "banners near me" filter, or are simply unused leftovers.
  • Whether a store-type banner's target store is checked for being active/open before the client opens it (unlike nearest_branch, which explicitly filters to active stores) — not established from this endpoint's code; the filtering, if any, would happen client-side or in the store-detail call itself.