diff --git a/Dockerfile b/Dockerfile index f563f50..fd43d0e 100755 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN npm ci COPY . . # Generate Prisma client -RUN npx prisma generate +RUN DATABASE_URL="postgresql://dummy:dummy@localhost/dummy" npx prisma generate # Build the application RUN npm run build @@ -38,7 +38,7 @@ RUN apk add --no-cache --virtual .build-deps python3 make g++ cairo-dev pango-de # Copy Prisma schema and generate client COPY prisma ./prisma -RUN npx prisma generate +RUN DATABASE_URL="postgresql://dummy:dummy@localhost/dummy" npx prisma generate # Copy built application COPY --from=builder /app/dist ./dist diff --git a/src/common/utils/match-status.util.ts b/src/common/utils/match-status.util.ts index 1db15fc..5d9a6cd 100644 --- a/src/common/utils/match-status.util.ts +++ b/src/common/utils/match-status.util.ts @@ -122,9 +122,8 @@ export const FINISHED_STATE_VALUES_FOR_DB = [ ]; function normalizeToken(value: unknown): string { - return String(value || "") - .trim() - .toLowerCase(); + if (typeof value !== "string") return ""; + return value.trim().toLowerCase(); } function parseScoreValue(value: ScoreLikeValue): number | null { diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index 69bd505..95d5e57 100755 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -307,28 +307,37 @@ export class AdminController { const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new NotFoundException("USER_NOT_FOUND"); - const validPlans = [PlanType.FREE, PlanType.PLUS, PlanType.PREMIUM, "past_due", "cancelled"]; + const validPlans = [ + PlanType.FREE, + PlanType.PLUS, + PlanType.PREMIUM, + "past_due", + "cancelled", + ]; const newPlan = data.plan as PlanType; if (!validPlans.includes(newPlan)) { throw new BadRequestException("INVALID_PLAN_TYPE"); } const updateData: any = { subscriptionStatus: newPlan }; - + if (data.expiresAt) { const parsedDate = new Date(data.expiresAt); - + // Business Logic: If upgrading to Premium/Plus, the expiry date cannot be in the past const today = new Date(); today.setHours(0, 0, 0, 0); // Strip time - + const expiry = new Date(parsedDate); expiry.setHours(0, 0, 0, 0); - if ((newPlan === PlanType.PREMIUM || newPlan === PlanType.PLUS) && expiry < today) { + if ( + (newPlan === PlanType.PREMIUM || newPlan === PlanType.PLUS) && + expiry < today + ) { throw new BadRequestException("EXPIRES_AT_CANNOT_BE_IN_PAST"); } - + updateData.subscriptionExpiresAt = parsedDate; } else if (data.expiresAt === null) { updateData.subscriptionExpiresAt = null; diff --git a/src/modules/admin/dto/update-user-subscription.dto.ts b/src/modules/admin/dto/update-user-subscription.dto.ts index 44b0c12..7b63831 100644 --- a/src/modules/admin/dto/update-user-subscription.dto.ts +++ b/src/modules/admin/dto/update-user-subscription.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsEnum, IsISO8601 } from "class-validator"; +import { IsString, IsOptional, IsISO8601 } from "class-validator"; import { ApiProperty } from "@nestjs/swagger"; export class UpdateUserSubscriptionDto { @@ -6,7 +6,10 @@ export class UpdateUserSubscriptionDto { @IsString() plan: string; - @ApiProperty({ description: "Expiration Date in ISO format", required: false }) + @ApiProperty({ + description: "Expiration Date in ISO format", + required: false, + }) @IsOptional() @IsISO8601() expiresAt?: string | null; diff --git a/src/modules/subscriptions/subscriptions.service.ts b/src/modules/subscriptions/subscriptions.service.ts index 4c3996d..9cca304 100644 --- a/src/modules/subscriptions/subscriptions.service.ts +++ b/src/modules/subscriptions/subscriptions.service.ts @@ -108,14 +108,10 @@ export class SubscriptionsService { await this.handleSubscriptionResumed(data); break; case "transaction.completed": - this.logger.log( - `Transaction completed: ${(data as Record).id}`, - ); + this.logger.log(`Transaction completed: ${data.id}`); break; case "transaction.payment_failed": - this.logger.warn( - `Payment failed for transaction: ${(data as Record).id}`, - ); + this.logger.warn(`Payment failed for transaction: ${data.id}`); break; default: this.logger.debug(`Unhandled Paddle event: ${eventType}`); @@ -218,7 +214,7 @@ export class SubscriptionsService { // Sync user subscription status await this.prisma.user.update({ where: { id: userId }, - data: { + data: { subscriptionStatus: effectivePlan, subscriptionExpiresAt: currentBillingPeriod?.ends_at ? new Date(currentBillingPeriod.ends_at) diff --git a/src/tasks/prediction-settlement.market-resolver.ts b/src/tasks/prediction-settlement.market-resolver.ts index 56ec40f..e4736a8 100644 --- a/src/tasks/prediction-settlement.market-resolver.ts +++ b/src/tasks/prediction-settlement.market-resolver.ts @@ -15,55 +15,74 @@ export interface PickRef { type Resolver = (pick: string, r: MatchResult) => boolean | null; const ms1x2: Resolver = (pick, r) => { - const outcome = r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X"; + const outcome = + r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X"; return pick === outcome; }; const ht1x2: Resolver = (pick, r) => { if (r.htScoreHome === null || r.htScoreAway === null) return null; const outcome = - r.htScoreHome > r.htScoreAway ? "1" : r.htScoreHome < r.htScoreAway ? "2" : "X"; + r.htScoreHome > r.htScoreAway + ? "1" + : r.htScoreHome < r.htScoreAway + ? "2" + : "X"; return pick === outcome; }; -const overUnder = (line: number): Resolver => (pick, r) => { - const total = r.scoreHome + r.scoreAway; - if (total === line) return null; - const isOver = total > line; - if (pick === "Üst" || pick === "Ust" || pick.toLowerCase() === "over") return isOver; - if (pick === "Alt" || pick.toLowerCase() === "under") return !isOver; - return null; -}; +const overUnder = + (line: number): Resolver => + (pick, r) => { + const total = r.scoreHome + r.scoreAway; + if (total === line) return null; + const isOver = total > line; + if (pick === "Üst" || pick === "Ust" || pick.toLowerCase() === "over") + return isOver; + if (pick === "Alt" || pick.toLowerCase() === "under") return !isOver; + return null; + }; -const overUnderHt = (line: number): Resolver => (pick, r) => { - if (r.htScoreHome === null || r.htScoreAway === null) return null; - const total = r.htScoreHome + r.htScoreAway; - if (total === line) return null; - const isOver = total > line; - if (pick === "Üst" || pick === "Ust" || pick.toLowerCase() === "over") return isOver; - if (pick === "Alt" || pick.toLowerCase() === "under") return !isOver; - return null; -}; +const overUnderHt = + (line: number): Resolver => + (pick, r) => { + if (r.htScoreHome === null || r.htScoreAway === null) return null; + const total = r.htScoreHome + r.htScoreAway; + if (total === line) return null; + const isOver = total > line; + if (pick === "Üst" || pick === "Ust" || pick.toLowerCase() === "over") + return isOver; + if (pick === "Alt" || pick.toLowerCase() === "under") return !isOver; + return null; + }; const btts: Resolver = (pick, r) => { const both = r.scoreHome > 0 && r.scoreAway > 0; - if (pick === "Var" || pick === "KG Var" || pick.toLowerCase() === "yes") return both; - if (pick === "Yok" || pick === "KG Yok" || pick.toLowerCase() === "no") return !both; + if (pick === "Var" || pick === "KG Var" || pick.toLowerCase() === "yes") + return both; + if (pick === "Yok" || pick === "KG Yok" || pick.toLowerCase() === "no") + return !both; return null; }; const htft: Resolver = (pick, r) => { if (r.htScoreHome === null || r.htScoreAway === null) return null; const ht = - r.htScoreHome > r.htScoreAway ? "1" : r.htScoreHome < r.htScoreAway ? "2" : "X"; - const ft = r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X"; + r.htScoreHome > r.htScoreAway + ? "1" + : r.htScoreHome < r.htScoreAway + ? "2" + : "X"; + const ft = + r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X"; const normalized = pick.replace(/\s/g, "").toUpperCase(); return normalized === `${ht}/${ft}`; }; const doubleChance: Resolver = (pick, r) => { - const ft = r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X"; - const normalized = pick.replace(/\s/g, "").toUpperCase().split(/[\/\-]/); + const ft = + r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X"; + const normalized = pick.replace(/\s/g, "").toUpperCase().split(/\/|-/); if (normalized.length !== 2) return null; return normalized.includes(ft); }; @@ -72,7 +91,8 @@ const oddEven: Resolver = (pick, r) => { const total = r.scoreHome + r.scoreAway; const isOdd = total % 2 === 1; if (pick === "Tek" || pick.toLowerCase() === "odd") return isOdd; - if (pick === "Çift" || pick === "Cift" || pick.toLowerCase() === "even") return !isOdd; + if (pick === "Çift" || pick === "Cift" || pick.toLowerCase() === "even") + return !isOdd; return null; }; @@ -105,7 +125,7 @@ export function resolveOutcomeForPick( pick: PickRef, result: MatchResult, ): boolean | null { - const market = pick.market.toUpperCase().replace(/[\s\-]/g, "_"); + const market = pick.market.toUpperCase().replace(/[\s-]/g, "_"); const resolver = resolvers[market] ?? resolvers[pick.market]; if (!resolver) return null; try { diff --git a/src/tasks/prediction-settlement.task.ts b/src/tasks/prediction-settlement.task.ts index d65ab17..958dac3 100644 --- a/src/tasks/prediction-settlement.task.ts +++ b/src/tasks/prediction-settlement.task.ts @@ -145,7 +145,9 @@ export class PredictionSettlementTask { return { market: String(main.market), pick: String(main.pick), - stake_units: Number(main.stake_units ?? advice.suggested_stake_units ?? 1), + stake_units: Number( + main.stake_units ?? advice.suggested_stake_units ?? 1, + ), odds: Number(main.odds ?? 0) || null, }; }