Files
sase.tr/apps/api/src/internal-admin/billing.controller.ts
Semih 854ccbbe12
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
feat(internal-admin): expired/cancelled kullanıcı için yeni abonelik başlatma ucu
POST /internal/admin/users/:id/subscriptions — panelin "Yeni abonelik başlat"
butonu için. manuallyActivate expired/cancelled'ı "yeni subscription
başlatın" diye reddediyordu ama bunu yapacak uç yoktu; manuel tanımlar
DB'den elle yapılıyordu.

- BillingService.startSubscription: active/trial varsa 409, plan aktif değilse
  409, Full plan → tüm aktif markalar, marka-limitli plan → brandIds tam sayı +
  aktif marka doğrulaması. days verilirse bitiş = şimdi + days (1..3650), yoksa
  billingPeriod default'u (aylık/yıllık). Gelir eventi (PostHog/Meta) ve
  referral kredisi tüketimi YOK — para hareketi olmayan founder tanımı.
- UserBillingController (internal/admin/users) + module kaydı.
- billing.service.spec.ts: 7 test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ww3tfpuxBcettz81dDvyYt
2026-09-19 00:45:11 +03:00

175 lines
5.1 KiB
TypeScript

import {
BadRequestException,
Body,
Controller,
HttpCode,
HttpStatus,
Param,
Post,
UseGuards,
} from "@nestjs/common";
import { Public } from "../common/decorators/public.decorator";
import { InternalTokenGuard } from "../common/guards/internal-token.guard";
import { BillingService } from "./billing.service";
@Controller("internal/admin/subscriptions")
@Public()
@UseGuards(InternalTokenGuard)
export class BillingController {
constructor(private billing: BillingService) {}
@Post(":id/trial/extend")
@HttpCode(HttpStatus.OK)
async extendTrial(
@Param("id") id: string,
@Body() body: { days?: number; reason?: string; founderId?: string },
) {
this.requireFounder(body);
this.requireReason(body, "trial-extend");
if (typeof body.days !== "number" || body.days <= 0) {
throw new BadRequestException("days must be a positive number");
}
return this.billing.extendTrial({
subscriptionId: id,
days: body.days,
reason: body.reason!,
founderId: body.founderId!,
});
}
@Post(":id/activate")
@HttpCode(HttpStatus.OK)
async manuallyActivate(
@Param("id") id: string,
@Body() body: { reason?: string; founderId?: string },
) {
this.requireFounder(body);
this.requireReason(body, "activate");
return this.billing.manuallyActivate({
subscriptionId: id,
reason: body.reason!,
founderId: body.founderId!,
});
}
@Post(":id/change-plan")
@HttpCode(HttpStatus.OK)
async changePlan(
@Param("id") id: string,
@Body() body: { newPlanId?: string; reason?: string; founderId?: string },
) {
this.requireFounder(body);
this.requireReason(body, "change-plan");
if (!body.newPlanId) throw new BadRequestException("newPlanId required");
return this.billing.changePlan({
subscriptionId: id,
newPlanId: body.newPlanId,
reason: body.reason!,
founderId: body.founderId!,
});
}
@Post(":id/cancel")
@HttpCode(HttpStatus.OK)
async cancel(@Param("id") id: string, @Body() body: { reason?: string; founderId?: string }) {
this.requireFounder(body);
this.requireReason(body, "cancel");
return this.billing.cancel({
subscriptionId: id,
reason: body.reason!,
founderId: body.founderId!,
});
}
@Post(":id/resume")
@HttpCode(HttpStatus.OK)
async resume(@Param("id") id: string, @Body() body: { reason?: string; founderId?: string }) {
this.requireFounder(body);
this.requireReason(body, "resume");
return this.billing.resume({
subscriptionId: id,
reason: body.reason!,
founderId: body.founderId!,
});
}
@Post(":id/brands")
@HttpCode(HttpStatus.OK)
async setBrands(
@Param("id") id: string,
@Body()
body: { brandIds?: string[]; reason?: string; founderId?: string },
) {
this.requireFounder(body);
this.requireReason(body, "set-brands");
if (!Array.isArray(body.brandIds)) {
throw new BadRequestException("brandIds array required");
}
return this.billing.setSubscriptionBrands({
subscriptionId: id,
brandIds: body.brandIds,
reason: body.reason!,
founderId: body.founderId!,
});
}
private requireFounder(body: { founderId?: string }) {
if (!body.founderId) throw new BadRequestException("founderId required");
}
private requireReason(body: { reason?: string }, action: string) {
if (!body.reason || body.reason.trim().length < 5) {
throw new BadRequestException(`${action} requires reason (min 5 chars)`);
}
}
}
/**
* User-scoped billing admin: start a fresh subscription for a user whose
* previous one is expired/cancelled (the panel's "Yeni abonelik başlat").
*/
@Controller("internal/admin/users")
@Public()
@UseGuards(InternalTokenGuard)
export class UserBillingController {
constructor(private billing: BillingService) {}
@Post(":id/subscriptions")
@HttpCode(HttpStatus.OK)
async startSubscription(
@Param("id") id: string,
@Body()
body: {
planId?: string;
billingPeriod?: string;
days?: number;
brandIds?: string[];
reason?: string;
founderId?: string;
},
) {
if (!body.founderId) throw new BadRequestException("founderId required");
if (!body.reason || body.reason.trim().length < 5) {
throw new BadRequestException("start-subscription requires reason (min 5 chars)");
}
if (!body.planId) throw new BadRequestException("planId required");
if (body.billingPeriod !== "monthly" && body.billingPeriod !== "yearly") {
throw new BadRequestException("billingPeriod must be monthly|yearly");
}
if (body.days !== undefined && (typeof body.days !== "number" || body.days <= 0)) {
throw new BadRequestException("days must be a positive number");
}
if (body.brandIds !== undefined && !Array.isArray(body.brandIds)) {
throw new BadRequestException("brandIds must be an array");
}
return this.billing.startSubscription({
userId: id,
planId: body.planId,
billingPeriod: body.billingPeriod,
days: body.days,
brandIds: body.brandIds,
reason: body.reason,
founderId: body.founderId,
});
}
}