product-repo.js 2.65 KB
import { billingProductTable } from '../db/index.js'

const PRODUCT = billingProductTable.tableName

export function createProductRepo(db, runner = db) {
  async function listProducts() {
    const rows = await runner(PRODUCT).where({ active: true }).orderBy('price_cents')
    return rows.map(productFromRow)
  }

  async function listAllProducts() {
    const rows = await runner(PRODUCT).orderBy('price_cents')
    return rows.map(productFromRow)
  }

  async function findProduct(id) {
    const row = await runner(PRODUCT).where({ id }).first()
    if (row === undefined) return null
    return productFromRow(row)
  }

  async function saveProduct(product) {
    await runner(PRODUCT).insert(productToRow(product)).onConflict('id').merge()
    return findProduct(product.id)
  }

  return { listProducts, listAllProducts, findProduct, saveProduct }
}

function productFromRow(row) {
  const id = requiredString(row.id, 'billing_product.id')
  const kind = requiredString(row.kind, 'billing_product.kind', id)
  const product = {
    id,
    kind,
    title: requiredString(row.title, 'billing_product.title', id),
    points: requiredInteger(row.points, 'billing_product.points', id),
    paid_points: requiredInteger(row.paid_points, 'billing_product.paid_points', id),
    bonus_points: requiredInteger(row.bonus_points, 'billing_product.bonus_points', id),
    price_cents: requiredInteger(row.price_cents, 'billing_product.price_cents', id),
    price_usd: Number(row.price_usd),
    currency: requiredString(row.currency, 'billing_product.currency', id),
    stripe_price_id: nullableString(row.stripe_price_id, 'billing_product.stripe_price_id', id),
    active: Boolean(row.active),
  }
  if (kind !== 'points_pack' || product.points !== product.paid_points + product.bonus_points) {
    throw new Error(`Invalid billing_product points_pack, id=${id}`)
  }
  return product
}

function productToRow(product) {
  if (product.kind !== 'points_pack') throw new Error(`Unsupported billing_product.kind: ${product.kind}, id: ${product.id}`)
  if (product.points !== product.paid_points + product.bonus_points) throw new Error(`Invalid billing_product points total, id=${product.id}`)
  return { ...product, updated_at: new Date() }
}

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)) throw new Error(`Invalid ${field}, id=${id}`)
  return number
}