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
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';

export class CreateRenewalHistory1700000000000 implements MigrationInterface {
name = 'CreateRenewalHistory1700000000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'renewal_history',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'gen_random_uuid()',
},
{
name: 'subscription_id',
type: 'uuid',
isNullable: false,
},
{
name: 'user_id',
type: 'uuid',
isNullable: false,
},
{
name: 'event_type',
type: 'text',
isNullable: false,
comment: "One of: 'renewed', 'failed', 'cancelled', 'paused', 'reminder_sent', 'reactivated'",
},
{
name: 'status',
type: 'text',
isNullable: true,
comment: "'success' | 'failed' | 'pending'",
},
{
name: 'amount',
type: 'decimal',
precision: 10,
scale: 2,
isNullable: true,
},
{
name: 'currency',
type: 'text',
isNullable: true,
},
{
name: 'payment_method',
type: 'text',
isNullable: true,
},
{
name: 'transaction_hash',
type: 'text',
isNullable: true,
},
{
name: 'blockchain_ledger',
type: 'integer',
isNullable: true,
},
{
name: 'channel',
type: 'text',
isNullable: true,
comment: "Populated for reminder_sent events — e.g. 'email', 'sms', 'push'",
},
{
name: 'blockchain_verified',
type: 'boolean',
default: false,
},
{
name: 'notes',
type: 'text',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamptz',
default: 'NOW()',
},
],
}),
true,
);

await queryRunner.createForeignKey(
'renewal_history',
new TableForeignKey({
columnNames: ['subscription_id'],
referencedTableName: 'subscriptions',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);

await queryRunner.createForeignKey(
'renewal_history',
new TableForeignKey({
columnNames: ['user_id'],
referencedTableName: 'profiles',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);

// Composite index for fast per-subscription timeline queries
await queryRunner.createIndex(
'renewal_history',
new TableIndex({
name: 'IDX_renewal_history_subscription_created',
columnNames: ['subscription_id', 'created_at'],
}),
);

// Index for per-user history queries
await queryRunner.createIndex(
'renewal_history',
new TableIndex({
name: 'IDX_renewal_history_user_created',
columnNames: ['user_id', 'created_at'],
}),
);

// Index for event_type filtering
await queryRunner.createIndex(
'renewal_history',
new TableIndex({
name: 'IDX_renewal_history_event_type',
columnNames: ['event_type'],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('renewal_history', true, true, true);
}
}
97 changes: 97 additions & 0 deletions backend/src/subscription-renewal-history-timeline/INTEGRATION.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// ─────────────────────────────────────────────────────────────────────────────
// INTEGRATION GUIDE — wire RenewalHistoryModule into your existing code
// ─────────────────────────────────────────────────────────────────────────────

// 1. app.module.ts ──────────────────────────────────────────────────────────
// Add RenewalHistoryModule alongside your existing modules:
//
// import { RenewalHistoryModule } from './renewal-history/renewal-history.module';
//
// @Module({
// imports: [
// ...existingModules,
// RenewalHistoryModule,
// ],
// })
// export class AppModule {}


// 2. subscriptions.module.ts ────────────────────────────────────────────────
// Import RenewalHistoryModule so SubscriptionsService can inject the service:
//
// @Module({
// imports: [TypeOrmModule.forFeature([Subscription]), RenewalHistoryModule],
// providers: [SubscriptionsService],
// controllers: [SubscriptionsController],
// })
// export class SubscriptionsModule {}


// 3. subscriptions.service.ts ───────────────────────────────────────────────
// Inject RenewalHistoryService and call record() on every renewal event:

import { Injectable } from '@nestjs/common';
import { RenewalHistoryService } from './renewal-history/renewal-history.service';
import { RenewalEventType, RenewalStatus } from './renewal-history/renewal-history.entity';

@Injectable()
export class SubscriptionsService {
constructor(
// ...your existing dependencies...
private readonly renewalHistory: RenewalHistoryService,
) {}

async processRenewal(subscriptionId: string, userId: string) {
try {
// ...existing Stellar payment logic...
const txHash = 'returned-from-stellar-sdk';
const ledger = 52345678;

// Record successful renewal
await this.renewalHistory.record({
subscriptionId,
userId,
eventType: RenewalEventType.RENEWED,
status: RenewalStatus.SUCCESS,
amount: 15.99,
currency: 'USD',
paymentMethod: 'stellar',
transactionHash: txHash,
blockchainLedger: ledger,
blockchainVerified: true,
});
} catch (err) {
// Record failed renewal
await this.renewalHistory.record({
subscriptionId,
userId,
eventType: RenewalEventType.FAILED,
status: RenewalStatus.FAILED,
notes: err instanceof Error ? err.message : String(err),
});
throw err;
}
}

async cancelSubscription(subscriptionId: string, userId: string, reason?: string) {
// ...cancellation logic...

await this.renewalHistory.record({
subscriptionId,
userId,
eventType: RenewalEventType.CANCELLED,
notes: reason,
});
}

async sendRenewalReminder(subscriptionId: string, userId: string, channel: 'email' | 'sms' | 'push') {
// ...send notification...

await this.renewalHistory.record({
subscriptionId,
userId,
eventType: RenewalEventType.REMINDER_SENT,
channel,
});
}
}
Loading
Loading