action-policy-repo.js 2.44 KB
import { billingActionPolicyTable } from '../db/index.js'

const POLICY = billingActionPolicyTable.tableName

export function createActionPolicyRepo(db, runner = db) {
  async function findActionPolicy(action) {
    const row = await runner(POLICY).where({ action }).first()
    if (row === undefined) return null
    return actionPolicyFromRow(row)
  }

  async function listActionPolicies() {
    const rows = await runner(POLICY).orderBy('action')
    return rows.map(actionPolicyFromRow)
  }

  async function updateActionPolicy(policy) {
    await runner(POLICY).where({ action: policy.action }).update({
      points: policy.points,
      period_kind: policy.period_kind,
      timezone: policy.timezone,
      enabled: policy.enabled,
      updated_by: policy.updated_by,
      updated_at: new Date(),
    })
    return findActionPolicy(policy.action)
  }

  return { findActionPolicy, listActionPolicies, updateActionPolicy }
}

function actionPolicyFromRow(row) {
  const action = requiredString(row.action, 'billing_action_policy.action')
  const policy = {
    action,
    points: requiredInteger(row.points, 'billing_action_policy.points', action),
    period_kind: requiredString(row.period_kind, 'billing_action_policy.period_kind', action),
    timezone: nullableString(row.timezone, 'billing_action_policy.timezone', action),
    enabled: Boolean(row.enabled),
    updated_by: nullableString(row.updated_by, 'billing_action_policy.updated_by', action),
  }
  assertActionPolicy(policy)
  return policy
}

function assertActionPolicy(policy) {
  switch (policy.period_kind) {
    case 'daily':
      if (policy.timezone === null) throw new Error(`Daily billing action requires timezone, id=${policy.action}`)
      return
    case 'none':
      if (policy.timezone !== null) throw new Error(`Non-periodic billing action cannot have timezone, id=${policy.action}`)
      return
    default:
      throw new Error(`Unhandled billing action period: ${policy.period_kind}, id: ${policy.action}`)
  }
}

function requiredString(value, field, id = 'unknown') {
  if (typeof value !== 'string' || value === '') throw new Error(`Invalid ${field}, id=${id}`)
  return value
}

function nullableString(value, field, id) {
  if (value === null) return null
  return requiredString(value, field, id)
}

function requiredInteger(value, field, id) {
  const number = Number(value)
  if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`Invalid ${field}, id=${id}`)
  return number
}