Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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
175 lines
5.1 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|