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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@ coverage/
*.lcov
coverage-final.json
clover.xml

# Local environment files. .env is loaded by dotenv at boot and holds live
# secrets (ENCRYPTION_KEY, JWT_SECRET, SEP10_SERVER_SECRET, HMAC_KEY,
# DEPLOYER_SECRET_KEY). The committed example file is the only environment
# file that belongs in git.
.env
.env.local
.env.*.local
!.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
-- Convert remaining free-form status strings to enums
-- This migration is designed to fail loudly if any existing row
-- contains a value outside the documented set, rather than coercing it.

-- ------------------------------------------------------------------------------
-- ContributionStatus: PENDING, COMPLETED, FAILED
-- ------------------------------------------------------------------------------
CREATE TYPE "ContributionStatus" AS ENUM ('PENDING', 'COMPLETED', 'FAILED');

-- Fail loudly if any existing row has an undocumented value.
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM "Contribution"
WHERE status NOT IN ('PENDING', 'COMPLETED', 'FAILED')
) THEN
RAISE EXCEPTION 'Contribution.status contains values outside the ContributionStatus enum';
END IF;
END $$;

ALTER TABLE "Contribution"
ALTER COLUMN status TYPE "ContributionStatus"
USING status::"ContributionStatus",
ALTER COLUMN status SET DEFAULT 'PENDING';

-- ------------------------------------------------------------------------------
-- PayoutStatus: COMPLETED
-- ------------------------------------------------------------------------------
CREATE TYPE "PayoutStatus" AS ENUM ('COMPLETED');

DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM "Payout"
WHERE status NOT IN ('COMPLETED')
) THEN
RAISE EXCEPTION 'Payout.status contains values outside the PayoutStatus enum';
END IF;
END $$;

ALTER TABLE "Payout"
ALTER COLUMN status TYPE "PayoutStatus"
USING status::"PayoutStatus";

-- ------------------------------------------------------------------------------
-- PayoutOrderLogReason: REORDER, SKIP_DEFAULT
-- ------------------------------------------------------------------------------
CREATE TYPE "PayoutOrderLogReason" AS ENUM ('REORDER', 'SKIP_DEFAULT');

DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM "PayoutOrderLog"
WHERE reason NOT IN ('REORDER', 'SKIP_DEFAULT')
) THEN
RAISE EXCEPTION 'PayoutOrderLog.reason contains values outside the PayoutOrderLogReason enum';
END IF;
END $$;

ALTER TABLE "PayoutOrderLog"
ALTER COLUMN reason TYPE "PayoutOrderLogReason"
USING reason::"PayoutOrderLogReason";

-- -----------------------------------------------------------------------------
-- MemberExitLogType: LEAVE, KICK
-- ------------------------------------------------------------------------------
CREATE TYPE "MemberExitLogType" AS ENUM ('LEAVE', 'KICK');

DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM "MemberExitLog"
WHERE type NOT IN ('LEAVE', 'KICK')
) THEN
RAISE EXCEPTION 'MemberExitLog.type contains values outside the MemberExitLogType enum';
END IF;
END $$;

ALTER TABLE "MemberExitLog"
ALTER COLUMN type TYPE "MemberExitLogType"
USING type::"MemberExitLogType";

-- ------------------------------------------------------------------------------
-- SecretAccessLogOperation: SEND, CONTRIBUTE, TRUSTLINE, KEY_ROTATION
-- ------------------------------------------------------------------------------
CREATE TYPE "SecretAccessLogOperation" AS ENUM ('SEND', 'CONTRIBUTE', 'TRUSTLINE', 'KEY_ROTATION');

DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM "SecretAccessLog"
WHERE operation NOT IN ('SEND', 'CONTRIBUTE', 'TRUSTLINE', 'KEY_ROTATION')
) THEN
RAISE EXCEPTION 'SecretAccessLog.operation contains values outside the SecretAccessLogOperation enum';
END IF;
END $$;}
37 changes: 32 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ enum DeploymentStatus {
FAILED
}

enum ContributionStatus {
PENDING
COMPLETED
FAILED
}

enum PayoutStatus {
COMPLETED
}

enum PayoutOrderReason {
REORDER
SKIP_DEFAULT
}

enum SecretAccessOperation {
SEND
CONTRIBUTE
TRUSTLINE
KEY_ROTATION
}

enum MemberExitType {
LEAVE
KICK
}

model SavingsGroup {
id String @id @default(uuid())
name String
Expand Down Expand Up @@ -89,7 +116,7 @@ model Contribution {
groupId String
amount Decimal
transactionHash String?
status String @default("PENDING") // PENDING, COMPLETED, FAILED
status ContributionStatus @default(PENDING)
createdAt DateTime @default(now())

@@index([userId])
Expand All @@ -104,7 +131,7 @@ model Payout {
amount Decimal
transactionHash String
cycleNumber Int
status String @default("COMPLETED")
status PayoutStatus @default(COMPLETED)
createdAt DateTime @default(now())

@@index([groupId])
Expand All @@ -118,7 +145,7 @@ model PayoutOrderLog {
changedBy String
previousOrder Json?
newOrder Json
reason String @default("REORDER") // e.g., "REORDER", "SKIP_DEFAULT"
reason PayoutOrderReason @default(REORDER)
createdAt DateTime @default(now())

@@index([groupId])
Expand Down Expand Up @@ -146,7 +173,7 @@ model PaymentRequest {
model SecretAccessLog {
id String @id @default(uuid())
userId String
operation String // "SEND" | "CONTRIBUTE" | "TRUSTLINE" | "KEY_ROTATION"
operation SecretAccessOperation
success Boolean
errorMessage String?
createdAt DateTime @default(now())
Expand Down Expand Up @@ -187,7 +214,7 @@ model MemberExitLog {
/// userId of who triggered the exit — the member themselves for LEAVE, the
/// creator for KICK.
initiatedBy String
type String // "LEAVE" | "KICK"
type MemberExitType
totalContributed Decimal
totalReceived Decimal
/// totalContributed - totalReceived. Positive is refunded to the member;
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/payout.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaClient } from '@prisma/client';
import { PrismaClient, PayoutOrderLogReason } from '@prisma/client';
import {
PayoutService,
PayoutOrderLockedError,
Expand Down Expand Up @@ -28,7 +28,7 @@ jest.mock('@prisma/client', () => {
},
$transaction: jest.fn((ops: any[]) => Promise.resolve(ops)),
};
return { PrismaClient: jest.fn(() => mPrismaClient), Prisma: { Decimal } };
return { PrismaClient: jest.fn(() => mPrismaClient), Prisma: { Decimal }, PayoutOrderLogReason: { REORDER: 'REORDER', SKIP_DEFAULT: 'SKIP_DEFAULT' } };
});

jest.mock('../queue/contribution-scheduler.queue', () => ({
Expand Down Expand Up @@ -153,7 +153,7 @@ describe('PayoutService', () => {
changedBy: 'u1',
previousOrder: ['u1', 'u2', 'u3'], // default join order since payoutOrder was null
newOrder: ['u3', 'u1', 'u2'],
reason: 'REORDER',
reason: PayoutOrderLogReason.REORDER,
},
});
});
Expand Down Expand Up @@ -387,7 +387,7 @@ describe('PayoutService', () => {
data: { payoutOrder: ['u1', 'u3', 'u2'] },
});
expect(prisma.payoutOrderLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({ reason: 'SKIP_DEFAULT', changedBy: 'u1' }),
data: expect.objectContaining({ reason: PayoutOrderLogReason.SKIP_DEFAULT, changedBy: 'u1' }),
});
});

Expand Down
3 changes: 2 additions & 1 deletion src/services/group-exit.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as StellarSdk from '@stellar/stellar-sdk';
import { prisma } from '../lib/prisma';
import { PayoutService } from './payout.service';
import { MemberExitType } from '@prisma/client';
import { SorobanService } from './soroban.service';
import { WhatsAppService } from './whatsapp.service';
import { removeGroupCycle } from '../queue/contribution-scheduler.queue';
Expand Down Expand Up @@ -166,7 +167,7 @@ export class GroupExitService {
groupId: group.id,
userId: member.userId,
initiatedBy,
type,
type: type as MemberExitType,
totalContributed: String(totalContributed),
totalReceived: String(totalReceived),
netOwed: String(netOwed),
Expand Down
8 changes: 4 additions & 4 deletions src/services/payout.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaClient, Prisma } from '@prisma/client';
import { PrismaClient, Prisma, PayoutOrderLogReason, PayoutStatus, ContributionStatus } from '@prisma/client';
import { WhatsAppService } from './whatsapp.service';
import { SorobanService } from './soroban.service';
import { config } from '../config/env';
Expand Down Expand Up @@ -100,7 +100,7 @@ export class PayoutService {
changedBy: requesterUserId,
previousOrder,
newOrder: order,
reason: 'REORDER',
reason: PayoutOrderLogReason.REORDER,
},
}),
]);
Expand Down Expand Up @@ -230,7 +230,7 @@ export class PayoutService {
changedBy: requesterUserId,
previousOrder: currentOrder,
newOrder,
reason: 'SKIP_DEFAULT',
reason: PayoutOrderLogReason.SKIP_DEFAULT,
},
}),
]);
Expand Down Expand Up @@ -345,7 +345,7 @@ export class PayoutService {
amount,
transactionHash: hash,
cycleNumber,
status: 'COMPLETED',
status: PayoutStatus.COMPLETED,
},
});

Expand Down
5 changes: 3 additions & 2 deletions src/utils/audit-logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { prisma } from '../lib/prisma';
import type { SecretAccessOperation } from '@prisma/client';

export type SecretOperation = 'SEND' | 'CONTRIBUTE' | 'TRUSTLINE' | 'KEY_ROTATION';
export type SecretOperation = SecretAccessOperation;

export async function logSecretAccess(
userId: string,
Expand All @@ -20,4 +21,4 @@ export async function logSecretAccess(
} catch (e) {
console.error('Audit log failed:', e);
}
}
}