App Version — client update gate (iOS / Android / Web)

The whole module reduces to one idea: a single public REST endpoint that the mobile/web clients poll to learn the latest published app version and whether the update is forced. It owns no data of its own — it is a thin read-only projection over the tenant Config document plus two hardcoded store URLs.

Source: BE src/modules/version · Admin — none (values are edited in the Config admin screen, not a dedicated "version" screen)

1. Purpose & scope

Lets the MSGold mobile apps (and the web client) check at startup whether they are running an outdated build and whether they must update before continuing.

  • Does: expose GET /api/version returning the current iOS/Android/Web version, a forceUpdate flag per platform, and a store downloadUrl per platform.
  • Does NOT: store version history / changelog (there is no Version collection — see §2), authenticate the caller, scope by branch, or push notifications. It is a stateless lookup. The version values live on the Config document and are managed by the Config module, not here.

This module is a NestJS REST controller only — there is no resolver, service, repository, or schema of its own:

src/modules/version/
  version.controller.ts   ← the single GET handler
  version.module.ts       ← imports ApConfigModule, registers the controller

version.module.ts (verbatim):

@Module({
  imports: [ApConfigModule],
  controllers: [VersionController],
})
export class VersionModule {}

Registered in app.module.ts (line 114). The app sets no global prefix (main.ts has no setGlobalPrefix), so the live route is exactly /api/version.

2. Data model

This module defines no collection. The data it returns comes from the Config collection (config/config.schema.ts), read via ApConfigService.findLast().

The four version-related fields on Config (config/config.schema.ts lines 12–19):

Field Type Required? Description
androidVersion string no Latest published Android version string (e.g. "1.4.2").
androidForceUpdate boolean no If true, Android clients below androidVersion must update.
iosVersion string no Latest published iOS version string.
iosForceUpdate boolean no If true, iOS clients below iosVersion must update.
// config/config.schema.ts (trimmed)
@ApSchema({ collection: `configs`, timestamps: true })
export class Config extends BaseSchema {
  @Prop({}) androidVersion: string;
  @Prop({}) androidForceUpdate: boolean;
  @Prop({}) iosVersion: string;
  @Prop({}) iosForceUpdate: boolean;
  // ...many other tenant settings...
}

There is no version/changelog historyConfig is a single per-company settings document; updating a version overwrites the previous value. findLast() returns the most recent Config for the company (config/config.service.ts):

override async findLast(query?: Partial<Config>): Promise<Config> {
  this.configRepo.ignoreBranchId = true;   // config is company-wide, not branch-scoped
  return await this.configRepo
    .findLast({ ...query, companyId: this.contextSvc?.user?.companyId })
    .finally(() => { this.configRepo.ignoreBranchId = false; });
}

Tenant note: findLast() filters by companyId from the request context. The /api/version endpoint has no auth context (see §5), so companyId is undefined and findLast returns the most recent Config document across the database. In practice the deployment is effectively single-tenant for this endpoint (the MSGold app).

3. API surface

REST

Method Route Body Auth Response
GET /api/version public (no guard) JSON: { ios, android, web }

The handler (version/version.controller.ts, verbatim):

@Controller("api/version")
export class VersionController {
  constructor(private readonly configSvc: ApConfigService) {}

  @Get("/")
  async version(): Promise<any> {
    const config = await this.configSvc.findLast();
    return {
      ios: {
        version: config.iosVersion,
        forceUpdate: config.iosForceUpdate,
        downloadUrl: "https://apps.apple.com/us/app/msgold/id6450125286",
      },
      android: {
        version: config.androidVersion,
        forceUpdate: config.androidForceUpdate,
        downloadUrl:
          "https://play.google.com/store/apps/details?id=com.msgold.app",
      },
      web: { v: "1.2", forceUpdate: true },
    };
  }
}

Example response shape:

{
  "ios":     { "version": "1.4.2", "forceUpdate": false, "downloadUrl": "https://apps.apple.com/us/app/msgold/id6450125286" },
  "android": { "version": "1.4.2", "forceUpdate": false, "downloadUrl": "https://play.google.com/store/apps/details?id=com.msgold.app" },
  "web":     { "v": "1.2", "forceUpdate": true }
}

No GraphQL surface in this module. The version values are written via the Config module's GraphQL mutation (updateConfig) — see §7.

4. Business rules & calculations

  • No server-side comparison. The endpoint reports the latest version + the forceUpdate flag; it does not receive the client's current version or compute "is outdated". The version comparison (clientVersion < serverVersion) happens on the client.
  • web is hardcoded{ v: "1.2", forceUpdate: true } is a literal in the controller, not read from Config. Bumping the web version requires a code change + redeploy.
  • Store URLs are hardcoded to the MSGold App Store / Play Store listings.
  • No validation, no state machine, no side effects. Pure read. Writing nothing, it emits no GL legs, stock rows, audit entries, or notifications.

There is no transactionality — it is a single findLast read.

5. Permissions

None. VersionController carries no @ApGqlAuthorize, no @UseGuards, and no permission module/action. It is a deliberately public, unauthenticated endpoint so an app can check for a forced update before the user logs in.

Contrast: the Config write path (updateConfig mutation) IS guarded and audited (@AuditMeta({ module: 'config', ... })) — see Config.

6. Flows

6.1 Client startup version check (happy path)

  1. App launches → client issues GET /api/version.
  2. VersionController.version() calls ApConfigService.findLast().
  3. findLast() sets ignoreBranchId = true, queries the configs collection for the latest doc (by companyId if present in context — absent here), returns the Config.
  4. Controller maps Config.{ios,android}Version + {ios,android}ForceUpdate into the response, appends hardcoded downloadUrls and the literal web block.
  5. Client compares its bundled build version against the returned version for its platform:
    • up to date → continue normally.
    • outdated and forceUpdate === true → block the app, show a blocking "Update required" screen linking to downloadUrl.
    • outdated and forceUpdate === false → show a dismissible "Update available" prompt.

6.2 Operator publishes a new version (the write side)

  1. Admin opens the Config screen (zerp-admin/src/modules/config, not the wingold module) and edits the Android/iOS version + force-update toggles.
  2. Admin saves → GraphQL updateConfig(UpdateConfigInput) mutation (config/config.resolver.ts), which writes the new androidVersion / iosVersion / *ForceUpdate onto Config (audited).
  3. Next time any app calls GET /api/version, the new values are served.

Unhappy paths

  • No Config document existsfindLast() returns null/undefined and the controller dereferences config.iosVersion, throwing → 500. There is no null guard in the controller. (Documented gotcha — see §9.)
  • Version fields unset on Config → the response carries version: null; the client decides what to do with a missing version.

7. Admin UI

There is no dedicated version admin module. The four fields are surfaced inside the general Config / settings screen:

  • The UpdateConfigInput (config/config.dto.ts, lines 138–144 and schema.gql line 11203+) exposes androidVersion, androidForceUpdate, iosVersion, iosForceUpdate.
  • They are edited and persisted through the Config context's update method and the updateConfig mutation.

See Config for the screen, Formik schema, and context methods.

8. Dependencies & integrations

  • Depends on: ApConfigModule / ApConfigService (the only injected dependency) → Config collection.
  • Consumed by: the MSGold mobile apps (iOS + Android) and the web client at startup. These are the external clients that poll /api/version.
  • External services: none called server-side. The hardcoded downloadUrls point at the App Store / Play Store listings; the apps open those URLs.
  • No cron / jobs / events.

9. Gotchas & project-specific rules

  • No Version collection / changelog. Despite the module name, nothing here records version history. It reports one current value per platform, stored on Config. If you need a changelog or staged rollout, that is net-new.
  • Web version + store URLs are hardcoded in the controller. Changing them needs a code change + redeploy, not a config edit.
  • Public + unauthenticated by design — must work pre-login for a forced-update gate. Don't add an auth guard without giving the apps an unauthenticated path.
  • findLast() is effectively global here — with no request context, companyId is undefined, so it returns the latest Config in the DB. This module assumes a single MSGold deployment; in a true multi-tenant setup you'd need to pass a company/app identifier into the endpoint.
  • No null guard — if no Config exists, the endpoint 500s. Seed a Config before relying on it.
  • Force-update logic lives on the client. The server only reports version + forceUpdate; the "is my build older?" comparison is the app's responsibility.