import { Prisma } from '@prisma/client'; import type { DashboardGranularity } from '@dukang/shared-types'; const COL_ALLOW = new Set([ 'u.created_at', 'o.created_at', 'o.paid_at', 'p.created_at', 's.created_at', 'r.created_at', ]); const SERIES_COL_ALLOW = new Set([ 'promo.promo_code_id', 'u.assoc_partner_account_id', 'ap.activity_poster_id', 's.partner_account_id', 'o.user_id', 'o.product_id', 'r.store_id', ]); export function sqlSeriesId(column: string): Prisma.Sql { if (!SERIES_COL_ALLOW.has(column)) throw new Error(`bad series col ${column}`); return Prisma.sql`COALESCE(CAST(${Prisma.raw(column)} AS CHAR), 'none')`; } export function shanghaiSqlBucket(column: string, grain: DashboardGranularity): Prisma.Sql { if (!COL_ALLOW.has(column)) throw new Error(`bad column ${column}`); const col = Prisma.raw(column); switch (grain) { case 'day': return Prisma.sql`DATE_FORMAT(CONVERT_TZ(${col}, '+00:00', '+08:00'), '%Y-%m-%d')`; case 'week': return Prisma.sql`DATE_FORMAT(DATE_SUB(DATE(CONVERT_TZ(${col}, '+00:00', '+08:00')), INTERVAL WEEKDAY(DATE(CONVERT_TZ(${col}, '+00:00', '+08:00'))) DAY), '%Y-%m-%d')`; case 'month': return Prisma.sql`DATE_FORMAT(CONVERT_TZ(${col}, '+00:00', '+08:00'), '%Y-%m')`; case 'quarter': return Prisma.sql`CONCAT(YEAR(CONVERT_TZ(${col}, '+00:00', '+08:00')), '-Q', QUARTER(CONVERT_TZ(${col}, '+00:00', '+08:00')))`; case 'year': return Prisma.sql`DATE_FORMAT(CONVERT_TZ(${col}, '+00:00', '+08:00'), '%Y')`; } } export type CityFilter = | { kind: 'all' } | { kind: 'none' } | { kind: 'empty' } | { kind: 'ids'; ids: bigint[]; codes: string[] }; export function sqlAnd(parts: Prisma.Sql[]): Prisma.Sql { return parts.length ? Prisma.join(parts, ' AND ') : Prisma.sql`1=1`; } export function sqlInBigints(columnSql: Prisma.Sql, ids: bigint[]): Prisma.Sql { if (!ids.length) return Prisma.sql`1=0`; return Prisma.sql`${columnSql} IN (${Prisma.join(ids)})`; } export function sqlInStrings(columnSql: Prisma.Sql, values: string[]): Prisma.Sql { if (!values.length) return Prisma.sql`1=0`; return Prisma.sql`${columnSql} IN (${Prisma.join(values)})`; } export function sqlRange(columnSql: Prisma.Sql, start: Date, endExclusive: Date): Prisma.Sql { return Prisma.sql`${columnSql} >= ${start} AND ${columnSql} < ${endExclusive}`; } export type PromoPick = { none?: boolean; id?: bigint }; export type UserWhereOpts = { city: CityFilter; promo?: PromoPick; assocPartnerId?: bigint; activityPosterId?: bigint; createdStart?: Date | null; createdEndExclusive?: Date | null; createdLte?: Date | null; }; export type OrderWhereOpts = { city: CityFilter; promo?: PromoPick; assocPartnerId?: bigint; activityPosterId?: bigint; productId?: bigint; createdStart?: Date | null; createdEndExclusive?: Date | null; }; function sqlAssocPartnerUsers(assocPartnerId: bigint): Prisma.Sql { return Prisma.sql`SELECT id FROM user_user WHERE assoc_partner_account_id = ${assocPartnerId} AND status = 1 AND merged_into_user_id IS NULL`; } function sqlActivityPosterUsers(activityPosterId: bigint): Prisma.Sql { return Prisma.sql`SELECT u.id FROM user_user u INNER JOIN partner_account p ON p.id = u.assoc_partner_account_id WHERE p.activity_poster_id = ${activityPosterId} AND p.is_primary = 1 AND u.status = 1 AND u.merged_into_user_id IS NULL`; } function sqlActivityPosterPartners(activityPosterId: bigint): Prisma.Sql { return Prisma.sql`SELECT id FROM partner_account WHERE activity_poster_id = ${activityPosterId} AND is_primary = 1`; } function applyPromo(parts: Prisma.Sql[], columnSql: Prisma.Sql, promo?: PromoPick) { if (promo?.none) parts.push(Prisma.sql`${columnSql} IS NULL`); else if (promo?.id !== undefined) parts.push(Prisma.sql`${columnSql} = ${promo.id}`); } export function userSqlWhere(opts: UserWhereOpts): Prisma.Sql { const parts: Prisma.Sql[] = [ Prisma.sql`u.status = 1`, Prisma.sql`u.merged_into_user_id IS NULL`, ]; if (opts.createdStart && opts.createdEndExclusive) { parts.push(sqlRange(Prisma.sql`u.created_at`, opts.createdStart, opts.createdEndExclusive)); } else if (opts.createdLte) { parts.push(Prisma.sql`u.created_at <= ${opts.createdLte}`); } if (opts.city.kind === 'empty') parts.push(Prisma.sql`1=0`); else if (opts.city.kind === 'none') { parts.push(Prisma.sql`(pref.id IS NULL OR pref.selected_city_code IS NULL)`); } else if (opts.city.kind === 'ids') { parts.push(sqlInStrings(Prisma.sql`pref.selected_city_code`, opts.city.codes)); } if (opts.promo?.none) parts.push(Prisma.sql`promo.id IS NULL`); else if (opts.promo?.id !== undefined) parts.push(Prisma.sql`promo.promo_code_id = ${opts.promo.id}`); if (opts.assocPartnerId !== undefined) { parts.push(Prisma.sql`u.assoc_partner_account_id = ${opts.assocPartnerId}`); } if (opts.activityPosterId !== undefined) { parts.push(Prisma.sql`u.assoc_partner_account_id IN (${sqlActivityPosterPartners(opts.activityPosterId)})`); } return sqlAnd(parts); } export function orderSqlWhere(opts: OrderWhereOpts): Prisma.Sql { const parts: Prisma.Sql[] = []; if (opts.createdStart && opts.createdEndExclusive) { parts.push(sqlRange(Prisma.sql`o.created_at`, opts.createdStart, opts.createdEndExclusive)); } if (opts.city.kind === 'none' || opts.city.kind === 'empty') parts.push(Prisma.sql`1=0`); else if (opts.city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`o.city_id`, opts.city.ids)); applyPromo(parts, Prisma.sql`o.promo_code_id`, opts.promo); if (opts.assocPartnerId !== undefined) { parts.push(Prisma.sql`o.user_id IN (${sqlAssocPartnerUsers(opts.assocPartnerId)})`); } if (opts.activityPosterId !== undefined) { parts.push(Prisma.sql`o.user_id IN (${sqlActivityPosterUsers(opts.activityPosterId)})`); } if (opts.productId !== undefined) parts.push(Prisma.sql`o.product_id = ${opts.productId}`); return sqlAnd(parts.length ? parts : [Prisma.sql`1=1`]); } export function paidOrderSqlWhere( opts: OrderWhereOpts & { paidStart: Date; paidEndExclusive: Date; partnerIdAtPay?: bigint; }, ): Prisma.Sql { const parts: Prisma.Sql[] = [ Prisma.sql`o.pay_status = 'PAID'`, sqlRange(Prisma.sql`o.paid_at`, opts.paidStart, opts.paidEndExclusive), ]; if (opts.city.kind === 'none' || opts.city.kind === 'empty') parts.push(Prisma.sql`1=0`); else if (opts.city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`o.city_id`, opts.city.ids)); applyPromo(parts, Prisma.sql`o.promo_code_id`, opts.promo); if (opts.partnerIdAtPay !== undefined) { parts.push(Prisma.sql`o.partner_account_id_at_pay = ${opts.partnerIdAtPay}`); } if (opts.assocPartnerId !== undefined) { parts.push(Prisma.sql`o.user_id IN (${sqlAssocPartnerUsers(opts.assocPartnerId)})`); } if (opts.activityPosterId !== undefined) { parts.push(Prisma.sql`o.user_id IN (${sqlActivityPosterUsers(opts.activityPosterId)})`); } if (opts.productId !== undefined) parts.push(Prisma.sql`o.product_id = ${opts.productId}`); return sqlAnd(parts); } export function partnerSqlWhere( city: CityFilter, partnerId: bigint | undefined, createdStart: Date | null, createdEndExclusive: Date | null, createdLte: Date | null, ): Prisma.Sql { const parts: Prisma.Sql[] = [Prisma.sql`p.is_primary = 1`]; if (createdStart && createdEndExclusive) { parts.push(sqlRange(Prisma.sql`p.created_at`, createdStart, createdEndExclusive)); } else if (createdLte) { parts.push(Prisma.sql`p.created_at <= ${createdLte}`); } if (city.kind === 'empty') parts.push(Prisma.sql`1=0`); else if (city.kind === 'none') parts.push(Prisma.sql`p.city_id IS NULL`); else if (city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`p.city_id`, city.ids)); if (partnerId !== undefined) parts.push(Prisma.sql`p.id = ${partnerId}`); return sqlAnd(parts); } export function storeSqlWhere( city: CityFilter, partnerId: bigint | undefined, createdStart: Date | null, createdEndExclusive: Date | null, createdLte: Date | null, ): Prisma.Sql { const parts: Prisma.Sql[] = []; if (createdStart && createdEndExclusive) { parts.push(sqlRange(Prisma.sql`s.created_at`, createdStart, createdEndExclusive)); } else if (createdLte) { parts.push(Prisma.sql`s.created_at <= ${createdLte}`); } if (city.kind === 'none' || city.kind === 'empty') parts.push(Prisma.sql`1=0`); else if (city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`s.city_id`, city.ids)); if (partnerId !== undefined) parts.push(Prisma.sql`s.partner_account_id = ${partnerId}`); return sqlAnd(parts.length ? parts : [Prisma.sql`1=1`]); } export function redeemSqlWhere( city: CityFilter, createdStart: Date, createdEndExclusive: Date, opts?: { storeId?: bigint; assocPartnerId?: bigint; storePartnerId?: bigint }, ): Prisma.Sql { const parts: Prisma.Sql[] = [ sqlRange(Prisma.sql`r.created_at`, createdStart, createdEndExclusive), ]; if (city.kind === 'none' || city.kind === 'empty') parts.push(Prisma.sql`1=0`); else if (city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`s.city_id`, city.ids)); if (opts?.storeId !== undefined) parts.push(Prisma.sql`r.store_id = ${opts.storeId}`); if (opts?.storePartnerId !== undefined) { parts.push(Prisma.sql`s.partner_account_id = ${opts.storePartnerId}`); } if (opts?.assocPartnerId !== undefined) { parts.push(Prisma.sql`r.user_id IN (${sqlAssocPartnerUsers(opts.assocPartnerId)})`); } return sqlAnd(parts); } export function money(v: unknown): number { if (v == null) return 0; const n = typeof v === 'number' ? v : Number(v); return Math.round(n * 100) / 100; } export function toCount(v: unknown): number { if (v == null) return 0; return typeof v === 'number' ? v : Number(v); }