refactor(api): collapse single-impl interfaces and facades (#3382)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 23:04:42 +02:00
1 parent e7b13fd19f
commit c16d414d93
426 files changed
+2085 -6445

No files matched your search

@@ -17,7 +17,6 @@ import {
DonorsByStripeCustomerId,
DonorsByStripeSubscriptionId,
} from '@app/api/donation/DonationTables';
import {IDonationRepository} from '@app/api/donation/IDonationRepository';
import {Donor} from '@app/api/donation/models/Donor';
import {DonorMagicLinkToken} from '@app/api/donation/models/DonorMagicLinkToken';
@@ -44,7 +43,7 @@ const FETCH_MAGIC_LINK_TOKENS_BY_EMAIL_QUERY = DonorMagicLinkTokensByEmail.selec
where: DonorMagicLinkTokensByEmail.where.eq('donor_email'),
});
export class DonationRepository extends IDonationRepository {
export class DonationRepository {
async findDonorByEmail(email: string): Promise<Donor | null> {
const row = await fetchOne<DonorRow>(FETCH_DONOR_BY_EMAIL_QUERY, {email});
return row ? new Donor(row) : null;
@@ -1,11 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IDonationService} from '@app/api/donation/IDonationService';
import type {DonationCheckoutService} from '@app/api/donation/services/DonationCheckoutService';
import type {DonationMagicLinkService} from '@app/api/donation/services/DonationMagicLinkService';
import type {DonationCurrency} from '@fluxer/schema/src/domains/donation/DonationSchemas';
export class DonationService implements IDonationService {
export class DonationService {
constructor(
private magicLinkService: DonationMagicLinkService,
private checkoutService: DonationCheckoutService,
@@ -1,70 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {Donor} from '@app/api/donation/models/Donor';
import type {DonorMagicLinkToken} from '@app/api/donation/models/DonorMagicLinkToken';
export abstract class IDonationRepository {
abstract findDonorByEmail(email: string): Promise<Donor | null>;
abstract findDonorByStripeCustomerId(customerId: string): Promise<Donor | null>;
abstract findDonorByStripeSubscriptionId(subscriptionId: string): Promise<Donor | null>;
abstract createDonor(data: {
email: string;
stripeCustomerId: string | null;
businessName?: string | null;
taxId?: string | null;
taxIdType?: string | null;
stripeSubscriptionId: string | null;
subscriptionAmountCents: number | null;
subscriptionCurrency: string | null;
subscriptionInterval: string | null;
subscriptionCurrentPeriodEnd: Date | null;
subscriptionCancelAt?: Date | null;
subscriptionStatus?: string | null;
}): Promise<Donor>;
abstract updateDonorSubscription(
email: string,
data: {
stripeCustomerId: string | null;
businessName?: string | null;
taxId?: string | null;
taxIdType?: string | null;
stripeSubscriptionId: string | null;
subscriptionAmountCents: number | null;
subscriptionCurrency: string | null;
subscriptionInterval: string | null;
subscriptionCurrentPeriodEnd: Date | null;
subscriptionCancelAt?: Date | null;
subscriptionStatus?: string | null;
},
): Promise<Donor | null>;
abstract updateDonorCustomerDetails(
email: string,
data: {
stripeCustomerId: string | null;
businessName?: string | null;
taxId?: string | null;
taxIdType?: string | null;
},
): Promise<Donor | null>;
abstract linkDonorStripeCustomer(email: string, stripeCustomerId: string): Promise<void>;
abstract clearDonorStripeCustomer(stripeCustomerId: string): Promise<void>;
abstract cancelDonorSubscription(email: string): Promise<void>;
abstract deleteDonorSubscriptionMapping(stripeSubscriptionId: string, email: string): Promise<void>;
abstract createMagicLinkToken(token: DonorMagicLinkToken): Promise<void>;
abstract findMagicLinkToken(token: string): Promise<DonorMagicLinkToken | null>;
abstract markMagicLinkTokenUsed(token: string, usedAt: Date): Promise<void>;
abstract invalidateTokensForEmail(email: string): Promise<void>;
}
@@ -1,20 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {DonationCurrency} from '@fluxer/schema/src/domains/donation/DonationSchemas';
export interface IDonationService {
requestMagicLink(email: string, locale?: string | null): Promise<void>;
validateMagicLinkToken(token: string): Promise<{
email: string;
stripeCustomerId: string | null;
}>;
redeemMagicLinkToken(token: string): Promise<string | null>;
createDonationCheckout(params: {
email: string;
amountCents: number;
currency: DonationCurrency;
interval: 'month' | 'year' | null;
isBusiness?: boolean;
locale?: string | null;
}): Promise<string>;
}
@@ -2,7 +2,7 @@
import {Config} from '@app/api/Config';
import {getContentMessage} from '@app/api/content_i18n/ContentI18n';
import type {IDonationRepository} from '@app/api/donation/IDonationRepository';
import type {DonationRepository} from '@app/api/donation/DonationRepository';
import type {IEmailDnsValidationService} from '@app/api/infrastructure/IEmailDnsValidationService';
import {getInstanceProductName} from '@app/api/instance/ProductName';
import {Logger} from '@app/api/Logger';
@@ -97,7 +97,7 @@ function getDonationProductData(interval: 'month' | 'year' | null, locale: strin
export class DonationCheckoutService {
constructor(
private stripe: Stripe | null,
private donationRepository: IDonationRepository,
private donationRepository: DonationRepository,
private emailDnsValidationService: IEmailDnsValidationService,
) {}
@@ -2,7 +2,7 @@
import {randomBytes} from 'node:crypto';
import {Config} from '@app/api/Config';
import type {IDonationRepository} from '@app/api/donation/IDonationRepository';
import type {DonationRepository} from '@app/api/donation/DonationRepository';
import {DonorMagicLinkToken} from '@app/api/donation/models/DonorMagicLinkToken';
import type {IEmailDnsValidationService} from '@app/api/infrastructure/IEmailDnsValidationService';
import {Logger} from '@app/api/Logger';
@@ -16,7 +16,7 @@ import type {IEmailService} from '@pkgs/email/src/IEmailService';
export class DonationMagicLinkService {
constructor(
private donationRepository: IDonationRepository,
private donationRepository: DonationRepository,
private emailService: IEmailService,
private emailDnsValidationService: IEmailDnsValidationService,
) {}
@@ -1,11 +1,6 @@
import {clearTestEmails, createUniqueEmail, listTestEmails} from '@app/api/auth/tests/AuthTestUtils';
import {DonationRepository} from '@app/api/donation/DonationRepository';
import {
clearDonationTestEmails,
createDonationRequestLinkBuilder,
createUniqueEmail,
listDonationTestEmails,
TEST_DONOR_EMAIL,
} from '@app/api/donation/tests/DonationTestUtils';
import {createDonationRequestLinkBuilder, TEST_DONOR_EMAIL} from '@app/api/donation/tests/DonationTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest';
@@ -21,7 +16,7 @@ describe('POST /donations/request-link', () => {
});
beforeEach(async () => {
await harness.reset();
await clearDonationTestEmails(harness);
await clearTestEmails(harness);
});
async function createDonor(email: string): Promise<void> {
await donationRepository.createDonor({
@@ -41,20 +36,20 @@ describe('POST /donations/request-link', () => {
test('sends magic link email when donor exists', async () => {
await createDonor(TEST_DONOR_EMAIL);
await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute();
const emails = await listDonationTestEmails(harness, {recipient: TEST_DONOR_EMAIL});
const emails = await listTestEmails(harness, {recipient: TEST_DONOR_EMAIL});
expect(emails).toHaveLength(1);
expect(emails[0]?.type).toBe('donation_magic_link');
});
test('does not send email when donor does not exist', async () => {
const unknownEmail = createUniqueEmail('unknown');
await createDonationRequestLinkBuilder(harness).body({email: unknownEmail}).expect(204).execute();
const emails = await listDonationTestEmails(harness, {recipient: unknownEmail});
const emails = await listTestEmails(harness, {recipient: unknownEmail});
expect(emails).toHaveLength(0);
});
test('magic link token is 64-character hex string', async () => {
await createDonor(TEST_DONOR_EMAIL);
await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute();
const emails = await listDonationTestEmails(harness, {recipient: TEST_DONOR_EMAIL});
const emails = await listTestEmails(harness, {recipient: TEST_DONOR_EMAIL});
const token = emails[0]?.metadata.token;
expect(token).toBeDefined();
expect(token).toHaveLength(64);
@@ -66,7 +61,7 @@ describe('POST /donations/request-link', () => {
await createDonor(TEST_DONOR_EMAIL);
await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute();
await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute();
const emails = await listDonationTestEmails(harness, {recipient: TEST_DONOR_EMAIL});
const emails = await listTestEmails(harness, {recipient: TEST_DONOR_EMAIL});
expect(emails).toHaveLength(2);
const token1 = emails[0]?.metadata.token;
const token2 = emails[1]?.metadata.token;
@@ -1,36 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {randomUUID} from 'node:crypto';
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
import {createBuilderWithoutAuth, type TestRequestBuilder} from '@app/api/test/TestRequestBuilder';
import {DONATION_CURRENCIES, type DonationCurrency} from '@fluxer/schema/src/domains/donation/DonationAmountUtils';
interface DonationTestEmailRecord {
to: string;
type: string;
timestamp: string;
metadata: Record<string, string>;
}
export async function listDonationTestEmails(
harness: ApiTestHarness,
params?: {
recipient?: string;
},
): Promise<Array<DonationTestEmailRecord>> {
const query = params?.recipient ? `?recipient=${encodeURIComponent(params.recipient)}` : '';
const response = await createBuilderWithoutAuth<{
emails: Array<DonationTestEmailRecord>;
}>(harness)
.get(`/test/emails${query}`)
.execute();
return response.emails;
}
export async function clearDonationTestEmails(harness: ApiTestHarness): Promise<void> {
await createBuilderWithoutAuth(harness).delete('/test/emails').expect(204).execute();
}
export const TEST_DONOR_EMAIL = '[email protected]';
export const TEST_MAGIC_LINK_TOKEN = 'a'.repeat(64);
export const DONATION_AMOUNTS = {
@@ -84,7 +57,3 @@ export function createValidCheckoutBody(overrides?: Partial<DonationCheckoutRequ
...overrides,
};
}
export function createUniqueEmail(prefix = 'donation'): string {
return `${prefix}-${randomUUID()}@test.com`;
}