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
27 changes: 27 additions & 0 deletions prisma/schema/instalment-schedule.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// prisma/schema/instalment-schedule.prisma
// Invoice payment schedule and InstalmentPaid event read model (#1177).

/// One row per invoice schedule instalment, updated/inserted as InstalmentPaid
/// contract events are indexed. Unpaid rows have paidAt = null; overdue status
/// is derived at request time by comparing dueDate against the current clock.
model InstalmentPayment {
id String @id @default(cuid())
invoiceId String
instalmentNumber Int
totalInstalments Int?
amount Decimal @db.Decimal(30, 7)
dueDate DateTime
paidAt DateTime?
payer String?
txHash String?
eventIndex Int?
ledger Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([invoiceId, instalmentNumber])
@@unique([txHash, eventIndex])
@@index([invoiceId, dueDate(sort: Asc)])
@@index([invoiceId, instalmentNumber(sort: Asc)])
@@map("instalment_payments")
}
4 changes: 3 additions & 1 deletion prisma/schema/key-collaboration.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ model KeyCoCreator {
@@map("key_cocreators")
}

/// One row per indexed TipSent event (#1132).
/// One row per indexed TipSent event (#1132, #1173).
model KeyTip {
id String @id @default(cuid())
/// Key ID (CreatorProfile.id) that received the tip.
Expand All @@ -55,6 +55,8 @@ model KeyTip {
tipper String
/// Tip amount in XLM (converted from stroops).
amount Decimal @db.Decimal(20, 7)
/// Whether the tipper chose to tip anonymously (#1173).
anonymous Boolean @default(false)
/// IPFS CID of the tip message, when one was attached.
messageHash String?
ledger Int?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- AlterTable
ALTER TABLE "key_tips" ADD COLUMN "anonymous" BOOLEAN NOT NULL DEFAULT false;

-- CreateTable
CREATE TABLE "instalment_payments" (
"id" TEXT NOT NULL,
"invoiceId" TEXT NOT NULL,
"instalmentNumber" INTEGER NOT NULL,
"totalInstalments" INTEGER,
"amount" DECIMAL(30,7) NOT NULL,
"dueDate" TIMESTAMP(3) NOT NULL,
"paidAt" TIMESTAMP(3),
"payer" TEXT,
"txHash" TEXT,
"eventIndex" INTEGER,
"ledger" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "instalment_payments_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "instalment_payments_invoiceId_instalmentNumber_key" ON "instalment_payments"("invoiceId", "instalmentNumber");

-- CreateIndex
CREATE UNIQUE INDEX "instalment_payments_txHash_eventIndex_key" ON "instalment_payments"("txHash", "eventIndex");

-- CreateIndex
CREATE INDEX "instalment_payments_invoiceId_dueDate_idx" ON "instalment_payments"("invoiceId", "dueDate" ASC);

-- CreateIndex
CREATE INDEX "instalment_payments_invoiceId_instalmentNumber_idx" ON "instalment_payments"("invoiceId", "instalmentNumber" ASC);
28 changes: 28 additions & 0 deletions src/constants/redis.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const contentAccessCachePattern = (keyId: string): string =>
// key_tips / key_tip_totals tables with a 60s TTL. IPFS tip message
// content fetched on demand is cached longer (immutable per CID).
export const KEY_TIPS_CACHE_TTL_SECONDS = 60;
export const KEY_TIPS_LEADERBOARD_CACHE_TTL_SECONDS = 300;
export const KEY_TIP_CONTENT_CACHE_TTL_SECONDS = 3600;

export const keyTipsListCacheKey = (
Expand All @@ -120,8 +121,35 @@ export const keyTipsListCacheKey = (
export const keyTipsTotalCacheKey = (keyId: string): string =>
`key:tips:${keyId}:total`;

export const keyTipsLeaderboardCacheKey = (
keyId: string,
limit: number
): string => `key:tips:${keyId}:leaderboard:${limit}`;

export const keyTipContentCacheKey = (messageHash: string): string =>
`key:tips:content:${messageHash}`;

export const keyTipsCachePattern = (keyId: string): string =>
`key:tips:${keyId}:*`;

// ── Key community polls endpoints (#1174) ───────────────────────
// GET /keys/:key_id/polls and /polls/:poll_id are cached for 30s and
// invalidated on PollVoteCast and PollClosed events.
export const KEY_POLLS_CACHE_TTL_SECONDS = 30;

export const keyPollsListCacheKey = (
keyId: string,
status: string,
page: number,
limit: number
): string => `key:polls:${keyId}:list:${status}:${page}:${limit}`;

export const keyPollDetailCacheKey = (
keyId: string,
pollId: string,
wallet: string
): string => `key:polls:${keyId}:detail:${pollId}:${wallet}`;

export const keyPollsCachePattern = (keyId: string): string =>
`key:polls:${keyId}:*`;

31 changes: 24 additions & 7 deletions src/modules/governance/governance-delegation-indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,20 @@ import {
delegateCachePattern,
delegatorsCachePattern,
delegationHistoryCachePattern,
votingWeightCachePattern,
} from './governance-delegation.service';

export const DELEGATION_SET_EVENT_TYPES: ReadonlySet<string> = new Set([
'DELEGATION_SET',
'VotingDelegated',
'VOTING_DELEGATED',
]);

export const DELEGATION_REVOKED_EVENT_TYPES: ReadonlySet<string> = new Set([
'DELEGATION_REVOKED',
'DelegationRevoked',
]);

// ── Typed event interfaces ────────────────────────────────────

/**
Expand All @@ -51,7 +63,7 @@ import {
* occurredAt — ISO-8601 timestamp
*/
export interface DelegationSetEvent extends IndexerChainEvent {
eventType: 'DELEGATION_SET';
eventType: 'DELEGATION_SET' | 'VotingDelegated' | 'VOTING_DELEGATED';
delegatorWallet: string;
delegateeWallet: string;
keyId: string;
Expand All @@ -70,7 +82,7 @@ export interface DelegationSetEvent extends IndexerChainEvent {
* occurredAt — ISO-8601 timestamp
*/
export interface DelegationRevokedEvent extends IndexerChainEvent {
eventType: 'DELEGATION_REVOKED';
eventType: 'DELEGATION_REVOKED' | 'DelegationRevoked';
delegatorWallet: string;
keyId: string;
occurredAt: string;
Expand All @@ -89,7 +101,7 @@ const DELEGATION_SET_REQUIRED_FIELDS: (keyof DelegationSetEvent)[] = [
];

/**
* Process a batch of DELEGATION_SET events.
* Process a batch of DELEGATION_SET / VotingDelegated events.
*
* For each event:
* 1. Validates required fields.
Expand All @@ -106,7 +118,7 @@ export async function processDelegationSetEvents(
events: IndexerChainEvent[]
): Promise<void> {
await processIndexerChainEvents(events, async event => {
if (event.eventType !== 'DELEGATION_SET') return;
if (!DELEGATION_SET_EVENT_TYPES.has(event.eventType)) return;

const e = event as DelegationSetEvent;

Expand Down Expand Up @@ -250,10 +262,13 @@ export async function processDelegationSetEvents(
delegatorsCachePattern(e.delegateeWallet),
delegationHistoryCachePattern(e.delegatorWallet),
delegationHistoryCachePattern(e.delegateeWallet),
votingWeightCachePattern(e.delegatorWallet),
votingWeightCachePattern(e.delegateeWallet),
];
if (previousDelegatee && previousDelegatee !== e.delegateeWallet) {
patternsToInvalidate.push(delegatorsCachePattern(previousDelegatee));
patternsToInvalidate.push(delegationHistoryCachePattern(previousDelegatee));
patternsToInvalidate.push(votingWeightCachePattern(previousDelegatee));
}
await cacheInvalidate(...patternsToInvalidate);

Expand Down Expand Up @@ -282,7 +297,7 @@ const DELEGATION_REVOKED_REQUIRED_FIELDS: (keyof DelegationRevokedEvent)[] = [
];

/**
* Process a batch of DELEGATION_REVOKED events.
* Process a batch of DELEGATION_REVOKED / DelegationRevoked events.
*
* For each event:
* 1. Validates required fields.
Expand All @@ -298,7 +313,7 @@ export async function processDelegationRevokedEvents(
events: IndexerChainEvent[]
): Promise<void> {
await processIndexerChainEvents(events, async event => {
if (event.eventType !== 'DELEGATION_REVOKED') return;
if (!DELEGATION_REVOKED_EVENT_TYPES.has(event.eventType)) return;

const e = event as DelegationRevokedEvent;

Expand Down Expand Up @@ -421,7 +436,9 @@ export async function processDelegationRevokedEvents(
delegateCachePattern(e.delegatorWallet),
delegatorsCachePattern(previousDelegatee),
delegationHistoryCachePattern(e.delegatorWallet),
delegationHistoryCachePattern(previousDelegatee)
delegationHistoryCachePattern(previousDelegatee),
votingWeightCachePattern(e.delegatorWallet),
votingWeightCachePattern(previousDelegatee)
);

logger.info(
Expand Down
Loading
Loading