Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion backend/services/billing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,16 @@ export type {
StreamExportOptions,
ReconciliationResult,
} from './accountingExportService';
export type { IMeteringService, IPricingService, ITaxService, IDunningService, IAccountingExportService } from './interfaces';
export {
BackendPartnerService,
} from './partnerService';
export type { SplitConfiguration, PartnerPayoutSchedule } from '../../../src/types/partner';
export type {
IMeteringService,
IPricingService,
ITaxService,
IDunningService,
IAccountingExportService,
IPartnerService,
} from './interfaces';
export { BillingError, BillingErrorCode } from './errors';
18 changes: 18 additions & 0 deletions backend/services/billing/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ReconciliationResult,
TransactionType,
} from './accountingExportService';
import { SplitConfiguration, PartnerPayoutSchedule } from '../../../src/types/partner';

export interface IMeteringService {
recordUsage(metric: UsageMetric): Promise<void>;
Expand Down Expand Up @@ -62,3 +63,20 @@ export interface IAccountingExportService {
expected: Array<{ id: string; amount: number; transactionType: TransactionType }>
): ReconciliationResult;
}

export interface IPartnerService {
executeSplitAtSettlement(input: {
splitConfiguration: SplitConfiguration;
transactionId: string;
grossAmount: number;
}): SplitExecution;
shouldSchedulePayout(config: SplitConfiguration, lastPayoutDate: Date | null): {
shouldProcess: boolean;
nextScheduledDate: Date;
reason: string;
};
aggregatePendingPayouts(
configurations: SplitConfiguration[],
grossAmount: number
): Map<string, number>;
}
152 changes: 152 additions & 0 deletions backend/services/billing/partnerService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type { SplitConfiguration, SplitExecution, PartnerPayoutSchedule } from '../../src/types/partner';
import { SplitEngine, type SplitResult } from '../../src/services/partnerService';

export interface PartnerSplitExecutionInput {
splitConfiguration: SplitConfiguration;
transactionId: string;
grossAmount: number;
}

export interface PayoutSchedulingResult {
shouldProcess: boolean;
nextScheduledDate: Date;
reason: string;
}

export class BackendPartnerService {
static executeSplitAtSettlement(input: PartnerSplitExecutionInput): SplitExecution {
const { splitConfiguration, transactionId, grossAmount } = input;

const result: SplitResult = SplitEngine.calculateSplit(splitConfiguration, grossAmount);

return {
id: `exec-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`,
splitConfigurationId: splitConfiguration.id,
subscriptionId: splitConfiguration.subscriptionId,
transactionId,
grossAmount,
splits: result.splits.map((s) => ({
partnerId: s.partnerId,
amount: s.amount,
percentage: s.percentage,
})),
platformRevenue: result.platformRevenue,
executedAt: new Date(),
status: 'completed',
};
}

static shouldSchedulePayout(
config: SplitConfiguration,
lastPayoutDate: Date | null
): PayoutSchedulingResult {
const now = new Date();

if (!config.isActive) {
return {
shouldProcess: false,
nextScheduledDate: now,
reason: 'Configuration is not active',
};
}

switch (config.payoutSchedule) {
case 'instant':
return {
shouldProcess: true,
nextScheduledDate: now,
reason: 'Instant payouts are processed immediately',
};

case 'daily': {
if (!lastPayoutDate) {
return {
shouldProcess: true,
nextScheduledDate: now,
reason: 'No previous payout found',
};
}
const diffMs = now.getTime() - lastPayoutDate.getTime();
const oneDayMs = 24 * 60 * 60 * 1000;
if (diffMs >= oneDayMs) {
return {
shouldProcess: true,
nextScheduledDate: new Date(lastPayoutDate.getTime() + oneDayMs),
reason: 'Daily threshold reached',
};
}
return {
shouldProcess: false,
nextScheduledDate: new Date(lastPayoutDate.getTime() + oneDayMs),
reason: 'Daily threshold not yet reached',
};
}

case 'weekly': {
if (!lastPayoutDate) {
return {
shouldProcess: true,
nextScheduledDate: now,
reason: 'No previous payout found',
};
}
const diffMs = now.getTime() - lastPayoutDate.getTime();
const oneWeekMs = 7 * 24 * 60 * 60 * 1000;
if (diffMs >= oneWeekMs) {
return {
shouldProcess: true,
nextScheduledDate: new Date(lastPayoutDate.getTime() + oneWeekMs),
reason: 'Weekly threshold reached',
};
}
return {
shouldProcess: false,
nextScheduledDate: new Date(lastPayoutDate.getTime() + oneWeekMs),
reason: 'Weekly threshold not yet reached',
};
}

case 'threshold': {
const threshold = config.minPayoutThreshold ?? 0;
if (threshold <= 0) {
return {
shouldProcess: true,
nextScheduledDate: now,
reason: 'No minimum threshold set',
};
}
return {
shouldProcess: false,
nextScheduledDate: now,
reason: `Pending balance must meet minimum threshold of ${threshold}`,
};
}

default:
return {
shouldProcess: false,
nextScheduledDate: now,
reason: 'Unknown payout schedule',
};
}
}

static aggregatePendingPayouts(
configurations: SplitConfiguration[],
grossAmount: number
): Map<string, number> {
const pendingByPartner = new Map<string, number>();

for (const config of configurations) {
if (!config.isActive) continue;

const result = SplitEngine.calculateSplit(config, grossAmount);
for (const split of result.splits) {
const current = pendingByPartner.get(split.partnerId) ?? 0;
pendingByPartner.set(split.partnerId, current + split.amount);
}
}

return pendingByPartner;
}
}
10 changes: 8 additions & 2 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const SandboxDashboardScreen = lazyScreen(() => import('../screens/SandboxDashbo
const ApiKeyManagementScreen = lazyScreen(() => import('../screens/ApiKeyManagementScreen'));
const DocumentationPortalScreen = lazyScreen(() => import('../screens/DocumentationPortalScreen'));
const IntegrationGuidesScreen = lazyScreen(() => import('../screens/IntegrationGuidesScreen'));
const PartnerDashboardScreen = lazyScreen(() => import('../screens/PartnerDashboardScreen'));
const PerformanceDashboardScreen = lazyScreen(
() => import('../screens/PerformanceDashboardScreen')
);
Expand Down Expand Up @@ -197,9 +198,14 @@ const HomeStack = () => (
<Stack.Screen
name="IntegrationGuides"
component={IntegrationGuidesScreen}
options={{ headerShown: false }}
options={{ title: 'Integrations', headerShown: true }}
/>
</Stack.Navigator>
<Stack.Screen
name="PartnerDashboard"
component={PartnerDashboardScreen}
options={{ title: 'Partner Dashboard', headerShown: true }}
/>
</Stack.Navigator>
);

const SettingsStack = () => (
Expand Down
1 change: 1 addition & 0 deletions src/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type RootStackParamList = {
ChangePlan: { subscriptionId: string };
PaymentMethods: undefined;
AnalyticsDashboard: undefined;
PartnerDashboard: undefined;
NotFound: { reason?: string };
};

Expand Down
Loading