fix(web): wire up next-intl i18n — install dep, add locale middleware, wrap next config
The i18n architecture (config, routing, translation files, locale pages) was already built but non-functional due to three missing pieces: 1. next-intl not listed in package.json 2. middleware.ts not using createMiddleware from next-intl/middleware 3. next.config.js not wrapped with createNextIntlPlugin Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -22,47 +22,60 @@ describe('SubscriptionEntity — lifecycle edge cases', () => {
|
||||
describe('markExpired', () => {
|
||||
it('transitions ACTIVE to EXPIRED', () => {
|
||||
const sub = makeSub('ACTIVE');
|
||||
sub.markExpired();
|
||||
const result = sub.markExpired();
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(sub.status).toBe('EXPIRED');
|
||||
});
|
||||
|
||||
it('transitions PAST_DUE to EXPIRED', () => {
|
||||
const sub = makeSub('PAST_DUE');
|
||||
sub.markExpired();
|
||||
const result = sub.markExpired();
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(sub.status).toBe('EXPIRED');
|
||||
});
|
||||
|
||||
it('throws when marking CANCELLED as expired', () => {
|
||||
it('returns error when marking CANCELLED as expired', () => {
|
||||
const sub = makeSub('CANCELLED');
|
||||
expect(() => sub.markExpired()).toThrow('Không thể đánh dấu hết hạn');
|
||||
const result = sub.markExpired();
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Không thể đánh dấu hết hạn');
|
||||
});
|
||||
|
||||
it('throws when already expired', () => {
|
||||
it('returns error when already expired', () => {
|
||||
const sub = makeSub('EXPIRED');
|
||||
expect(() => sub.markExpired()).toThrow('Không thể đánh dấu hết hạn');
|
||||
const result = sub.markExpired();
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Không thể đánh dấu hết hạn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('markPastDue', () => {
|
||||
it('transitions ACTIVE to PAST_DUE', () => {
|
||||
const sub = makeSub('ACTIVE');
|
||||
sub.markPastDue();
|
||||
const result = sub.markPastDue();
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(sub.status).toBe('PAST_DUE');
|
||||
});
|
||||
|
||||
it('throws when marking PAST_DUE as past due again', () => {
|
||||
it('returns error when marking PAST_DUE as past due again', () => {
|
||||
const sub = makeSub('PAST_DUE');
|
||||
expect(() => sub.markPastDue()).toThrow('Không thể đánh dấu quá hạn');
|
||||
const result = sub.markPastDue();
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Không thể đánh dấu quá hạn');
|
||||
});
|
||||
|
||||
it('throws when marking CANCELLED as past due', () => {
|
||||
it('returns error when marking CANCELLED as past due', () => {
|
||||
const sub = makeSub('CANCELLED');
|
||||
expect(() => sub.markPastDue()).toThrow('Không thể đánh dấu quá hạn');
|
||||
const result = sub.markPastDue();
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Không thể đánh dấu quá hạn');
|
||||
});
|
||||
|
||||
it('throws when marking EXPIRED as past due', () => {
|
||||
it('returns error when marking EXPIRED as past due', () => {
|
||||
const sub = makeSub('EXPIRED');
|
||||
expect(() => sub.markPastDue()).toThrow('Không thể đánh dấu quá hạn');
|
||||
const result = sub.markPastDue();
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Không thể đánh dấu quá hạn');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,14 +152,16 @@ describe('SubscriptionEntity — lifecycle edge cases', () => {
|
||||
});
|
||||
|
||||
describe('upgrade constraints', () => {
|
||||
it('throws when upgrading from PAST_DUE', () => {
|
||||
it('returns error when upgrading from PAST_DUE', () => {
|
||||
const sub = makeSub('PAST_DUE');
|
||||
expect(() => sub.upgrade('plan-2', 'ENTERPRISE')).toThrow();
|
||||
const result = sub.upgrade('plan-2', 'ENTERPRISE');
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it('throws when upgrading from EXPIRED', () => {
|
||||
it('returns error when upgrading from EXPIRED', () => {
|
||||
const sub = makeSub('EXPIRED');
|
||||
expect(() => sub.upgrade('plan-2', 'ENTERPRISE')).toThrow();
|
||||
const result = sub.upgrade('plan-2', 'ENTERPRISE');
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -33,8 +33,9 @@ describe('SubscriptionEntity', () => {
|
||||
const sub = makeSub();
|
||||
sub.clearDomainEvents();
|
||||
|
||||
sub.upgrade('plan-2', 'ENTERPRISE');
|
||||
const result = sub.upgrade('plan-2', 'ENTERPRISE');
|
||||
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(sub.planId).toBe('plan-2');
|
||||
expect(sub.planTier).toBe('ENTERPRISE');
|
||||
const events = sub.domainEvents;
|
||||
@@ -42,19 +43,21 @@ describe('SubscriptionEntity', () => {
|
||||
expect(events[0].eventName).toBe('subscription.upgraded');
|
||||
});
|
||||
|
||||
it('throws when upgrading non-active subscription', () => {
|
||||
it('returns error when upgrading non-active subscription', () => {
|
||||
const sub = makeSub();
|
||||
sub.cancel();
|
||||
|
||||
expect(() => sub.upgrade('plan-2', 'ENTERPRISE')).toThrow();
|
||||
const result = sub.upgrade('plan-2', 'ENTERPRISE');
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it('cancels subscription', () => {
|
||||
const sub = makeSub();
|
||||
sub.clearDomainEvents();
|
||||
|
||||
sub.cancel();
|
||||
const cancelResult = sub.cancel();
|
||||
|
||||
expect(cancelResult.isOk).toBe(true);
|
||||
expect(sub.status).toBe('CANCELLED');
|
||||
expect(sub.cancelledAt).not.toBeNull();
|
||||
const events = sub.domainEvents;
|
||||
@@ -62,22 +65,26 @@ describe('SubscriptionEntity', () => {
|
||||
expect(events[0].eventName).toBe('subscription.cancelled');
|
||||
});
|
||||
|
||||
it('throws when cancelling already cancelled subscription', () => {
|
||||
it('returns error when cancelling already cancelled subscription', () => {
|
||||
const sub = makeSub();
|
||||
sub.cancel();
|
||||
|
||||
expect(() => sub.cancel()).toThrow('Subscription đã bị hủy');
|
||||
const result = sub.cancel();
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Subscription đã bị hủy');
|
||||
});
|
||||
|
||||
it('marks past due', () => {
|
||||
const sub = makeSub();
|
||||
sub.markPastDue();
|
||||
const pdResult = sub.markPastDue();
|
||||
expect(pdResult.isOk).toBe(true);
|
||||
expect(sub.status).toBe('PAST_DUE');
|
||||
});
|
||||
|
||||
it('marks expired', () => {
|
||||
const sub = makeSub();
|
||||
sub.markExpired();
|
||||
const expResult = sub.markExpired();
|
||||
expect(expResult.isOk).toBe(true);
|
||||
expect(sub.status).toBe('EXPIRED');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
import {
|
||||
type PlanTier,
|
||||
type SubscriptionStatus,
|
||||
} from '@prisma/client';
|
||||
import { AggregateRoot } from '@modules/shared/domain/aggregate-root';
|
||||
import { AggregateRoot, DomainException, ErrorCode, Result } from '@modules/shared';
|
||||
import { SubscriptionCancelledEvent } from '../events/subscription-cancelled.event';
|
||||
import { SubscriptionCreatedEvent } from '../events/subscription-created.event';
|
||||
import { SubscriptionUpgradedEvent } from '../events/subscription-upgraded.event';
|
||||
@@ -70,9 +71,15 @@ export class SubscriptionEntity extends AggregateRoot<string> {
|
||||
return subscription;
|
||||
}
|
||||
|
||||
upgrade(newPlanId: string, newPlanTier: PlanTier): void {
|
||||
upgrade(newPlanId: string, newPlanTier: PlanTier): Result<void, DomainException> {
|
||||
if (this._status !== 'ACTIVE') {
|
||||
throw new Error(`Không thể nâng cấp subscription ở trạng thái ${this._status}`);
|
||||
return Result.err(
|
||||
new DomainException(
|
||||
ErrorCode.SUBSCRIPTION_INACTIVE,
|
||||
`Không thể nâng cấp subscription ở trạng thái ${this._status}`,
|
||||
HttpStatus.CONFLICT,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const oldTier = this._planTier;
|
||||
@@ -83,11 +90,18 @@ export class SubscriptionEntity extends AggregateRoot<string> {
|
||||
this.addDomainEvent(
|
||||
new SubscriptionUpgradedEvent(this.id, this._userId, oldTier, newPlanTier),
|
||||
);
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
cancel(): Result<void, DomainException> {
|
||||
if (this._status === 'CANCELLED') {
|
||||
throw new Error('Subscription đã bị hủy');
|
||||
return Result.err(
|
||||
new DomainException(
|
||||
ErrorCode.SUBSCRIPTION_ALREADY_CANCELLED,
|
||||
'Subscription đã bị hủy',
|
||||
HttpStatus.CONFLICT,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this._status = 'CANCELLED';
|
||||
@@ -97,22 +111,37 @@ export class SubscriptionEntity extends AggregateRoot<string> {
|
||||
this.addDomainEvent(
|
||||
new SubscriptionCancelledEvent(this.id, this._userId, this._planTier),
|
||||
);
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
markExpired(): void {
|
||||
markExpired(): Result<void, DomainException> {
|
||||
if (this._status !== 'ACTIVE' && this._status !== 'PAST_DUE') {
|
||||
throw new Error(`Không thể đánh dấu hết hạn ở trạng thái ${this._status}`);
|
||||
return Result.err(
|
||||
new DomainException(
|
||||
ErrorCode.SUBSCRIPTION_INACTIVE,
|
||||
`Không thể đánh dấu hết hạn ở trạng thái ${this._status}`,
|
||||
HttpStatus.CONFLICT,
|
||||
),
|
||||
);
|
||||
}
|
||||
this._status = 'EXPIRED';
|
||||
this.updatedAt = new Date();
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
markPastDue(): void {
|
||||
markPastDue(): Result<void, DomainException> {
|
||||
if (this._status !== 'ACTIVE') {
|
||||
throw new Error(`Không thể đánh dấu quá hạn ở trạng thái ${this._status}`);
|
||||
return Result.err(
|
||||
new DomainException(
|
||||
ErrorCode.SUBSCRIPTION_INACTIVE,
|
||||
`Không thể đánh dấu quá hạn ở trạng thái ${this._status}`,
|
||||
HttpStatus.CONFLICT,
|
||||
),
|
||||
);
|
||||
}
|
||||
this._status = 'PAST_DUE';
|
||||
this.updatedAt = new Date();
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
renewPeriod(newStart: Date, newEnd: Date): void {
|
||||
|
||||
Reference in New Issue
Block a user