feat: implement order notifications service with TTO reservation confirmation handling feat: create migration for TTO booking email webhook trigger
135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|
|
|
import type { AccountBalanceEntry } from '../../types/account-balance-entry';
|
|
|
|
export type AccountBalanceSummary = {
|
|
totalBalance: number;
|
|
expiringSoon: number;
|
|
recentEntries: AccountBalanceEntry[];
|
|
};
|
|
|
|
export class AccountBalanceService {
|
|
private supabase: ReturnType<typeof getSupabaseServerClient>;
|
|
|
|
constructor() {
|
|
this.supabase = getSupabaseServerClient();
|
|
}
|
|
|
|
/**
|
|
* Get the current balance for a specific account
|
|
*/
|
|
async getAccountBalance(accountId: string): Promise<number> {
|
|
const { data, error } = await this.supabase
|
|
.schema('medreport')
|
|
.rpc('get_account_balance', {
|
|
p_account_id: accountId,
|
|
});
|
|
|
|
if (error) {
|
|
console.error('Error getting account balance:', error);
|
|
throw new Error('Failed to get account balance');
|
|
}
|
|
|
|
return data || 0;
|
|
}
|
|
|
|
/**
|
|
* Get balance entries for an account with pagination
|
|
*/
|
|
async getAccountBalanceEntries(
|
|
accountId: string,
|
|
options: {
|
|
limit?: number;
|
|
offset?: number;
|
|
entryType?: string;
|
|
includeInactive?: boolean;
|
|
} = {},
|
|
): Promise<{
|
|
entries: AccountBalanceEntry[];
|
|
total: number;
|
|
}> {
|
|
const {
|
|
limit = 50,
|
|
offset = 0,
|
|
entryType,
|
|
includeInactive = false,
|
|
} = options;
|
|
|
|
let query = this.supabase
|
|
.schema('medreport')
|
|
.from('account_balance_entries')
|
|
.select('*', { count: 'exact' })
|
|
.eq('account_id', accountId)
|
|
.order('created_at', { ascending: false })
|
|
.range(offset, offset + limit - 1);
|
|
|
|
if (!includeInactive) {
|
|
query = query.eq('is_active', true);
|
|
}
|
|
|
|
if (entryType) {
|
|
query = query.eq('entry_type', entryType);
|
|
}
|
|
|
|
const { data, error, count } = await query;
|
|
|
|
if (error) {
|
|
console.error('Error getting account balance entries:', error);
|
|
throw new Error('Failed to get account balance entries');
|
|
}
|
|
|
|
return {
|
|
entries: data || [],
|
|
total: count || 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get balance summary for dashboard display
|
|
*/
|
|
async getBalanceSummary(accountId: string): Promise<AccountBalanceSummary> {
|
|
const [balance, entries] = await Promise.all([
|
|
this.getAccountBalance(accountId),
|
|
this.getAccountBalanceEntries(accountId, { limit: 5 }),
|
|
]);
|
|
|
|
// Calculate expiring balance (next 30 days)
|
|
const thirtyDaysFromNow = new Date();
|
|
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
|
|
|
|
const { data: expiringData, error: expiringError } = await this.supabase
|
|
.schema('medreport')
|
|
.from('account_balance_entries')
|
|
.select('amount')
|
|
.eq('account_id', accountId)
|
|
.eq('is_active', true)
|
|
.not('expires_at', 'is', null)
|
|
.lte('expires_at', thirtyDaysFromNow.toISOString());
|
|
|
|
if (expiringError) {
|
|
console.error('Error getting expiring balance:', expiringError);
|
|
}
|
|
|
|
const expiringSoon =
|
|
expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
|
|
|
|
return {
|
|
totalBalance: balance,
|
|
expiringSoon,
|
|
recentEntries: entries.entries,
|
|
};
|
|
}
|
|
|
|
async processPeriodicBenefitDistributions(): Promise<void> {
|
|
console.info('Processing periodic benefit distributions...');
|
|
const { error } = await this.supabase
|
|
.schema('medreport')
|
|
.rpc('process_periodic_benefit_distributions');
|
|
if (error) {
|
|
console.error('Error processing periodic benefit distributions:', error);
|
|
throw new Error('Failed to process periodic benefit distributions');
|
|
}
|
|
console.info('Periodic benefit distributions processed successfully');
|
|
}
|
|
}
|