feat(FN-207): Step 1 — add downgradeOffer i18n keys (tr + en) (+4 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

Commits merged:
- style(FN-207): apply Biome formatting
- feat(FN-207): Steps 4-9 — add downgrade save-flow UI (helper, dialogs, plans query, handler)
- feat(FN-207): Step 3 — add PATCH /subscriptions/downgrade and GET /subscriptions/plans endpoints
- feat(FN-207): Step 2 — add downgrade() and getPlans() service methods
- feat(FN-207): Step 1 — add downgradeOffer i18n keys (tr + en)

Files changed:
.../src/subscriptions/subscriptions.controller.ts  |  13 ++
 .../api/src/subscriptions/subscriptions.service.ts |  75 ++++++
 apps/web/src/messages/en.json                      |  10 +
 apps/web/src/messages/tr.json                      |  10 +
 apps/web/src/routes/dashboard/index.tsx            |  16 +-
 .../src/routes/dashboard/subscription/index.tsx    | 255 ++++++++++++++++++---
 6 files changed, 340 insertions(+), 39 deletions(-)

Fusion-Task-Id: FN-207
This commit is contained in:
Fusion
2026-05-12 21:29:04 +00:00
parent 396049a77a
commit 405c0067dc
6 changed files with 340 additions and 39 deletions

View File

@@ -49,6 +49,19 @@ export class SubscriptionsController {
return this.subscriptionsService.resume(userId);
}
@Patch("downgrade")
async downgrade(
@CurrentUser("id") userId: string,
@Body() body: { planId: string; brandIds: string[] },
) {
return this.subscriptionsService.downgrade(userId, body);
}
@Get("plans")
async getPlans() {
return this.subscriptionsService.getPlans();
}
@Get()
@UseGuards(RolesGuard)
@Roles("admin")

View File

@@ -308,6 +308,81 @@ export class SubscriptionsService {
return subscription;
}
async downgrade(userId: string, data: { planId: string; brandIds: string[] }) {
// Find active subscription
const [sub] = await this.db
.select()
.from(userSubscriptions)
.where(and(eq(userSubscriptions.userId, userId), eq(userSubscriptions.status, "active")))
.limit(1);
if (!sub) throw new NotFoundException("Aktif abonelik bulunamadı");
// Validate target plan exists
const [targetPlan] = await this.db
.select()
.from(plans)
.where(eq(plans.id, data.planId))
.limit(1);
if (!targetPlan) throw new NotFoundException("Plan bulunamadı");
// Validate brand count matches plan
if (targetPlan.brandCount > 0 && data.brandIds.length !== targetPlan.brandCount) {
throw new BadRequestException(`Bu plan tam olarak ${targetPlan.brandCount} marka gerektirir`);
}
// Update subscription plan
const [updated] = await this.db
.update(userSubscriptions)
.set({ planId: data.planId, updatedAt: new Date() })
.where(eq(userSubscriptions.id, sub.id))
.returning();
// Replace brand associations
await this.db.delete(userBrands).where(eq(userBrands.subscriptionId, sub.id));
if (data.brandIds.length > 0) {
await this.db.insert(userBrands).values(
data.brandIds.map((brandId) => ({
userId,
subscriptionId: sub.id,
brandId,
})),
);
}
return updated;
}
async getPlans() {
const rows = await this.db
.select({
id: plans.id,
name: plans.name,
brandCount: plans.brandCount,
priceMonthly: plans.priceMonthly,
priceYearly: plans.priceYearly,
})
.from(plans)
.where(eq(plans.isActive, true));
// Map DB name to frontend key
const nameToKey: Record<string, string> = {
"1 Marka": "brand1",
"2 Marka": "brand2",
"3 Marka": "brand3",
"Full Paket": "full",
};
return rows
.filter((p) => nameToKey[p.name])
.map((p) => ({
...p,
key: nameToKey[p.name],
}));
}
async findAll(page = 1, limit = 20) {
const offset = (page - 1) * limit;
const items = await this.db