Skip to content

Commit 36f393d

Browse files
devin-ai-integration[bot]jujunjun110
andcommittedDec 23, 2025·
test: webapp/src/server配下のusecasesとutilsにテストを追加
以下のモジュールにテストを追加: - get-sankey-aggregation-usecase.ts - get-transactions-by-slug-usecase.ts - get-monthly-transaction-aggregation-usecase.ts - get-balance-sheet-usecase.ts - get-all-transactions-by-slug-usecase.ts - get-transactions-for-csv-usecase.ts - sankey-id-utils.ts テスト方針: - モジュールの主要な責務を確認するテストを中心に作成 - ハッピーケースとエッジケースを重点的にカバー - infra層(リポジトリ実装)のテストは除外 - 100%カバレッジは目指さない Co-Authored-By: jujunjun110@gmail.com <jujunjun110@gmail.com>
1 parent 6564cf8 commit 36f393d

7 files changed

+1151
-0
lines changed
 
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { GetAllTransactionsBySlugUsecase } from "@/server/usecases/get-all-transactions-by-slug-usecase";
2+
import type { ITransactionRepository } from "@/server/repositories/interfaces/transaction-repository.interface";
3+
import type { IPoliticalOrganizationRepository } from "@/server/repositories/interfaces/political-organization-repository.interface";
4+
import type { Transaction } from "@/shared/models/transaction";
5+
6+
const createMockTransaction = (overrides: Partial<Transaction> = {}): Transaction => ({
7+
id: "test-id-1",
8+
political_organization_id: "org-1",
9+
transaction_no: "T001",
10+
transaction_date: new Date("2025-08-15"),
11+
financial_year: 2025,
12+
transaction_type: "expense",
13+
debit_account: "政治活動費",
14+
debit_amount: 100000,
15+
credit_account: "現金",
16+
credit_amount: 100000,
17+
friendly_category: "支出",
18+
category_key: "political-activity",
19+
label: "テスト取引",
20+
hash: "test-hash",
21+
created_at: new Date(),
22+
updated_at: new Date(),
23+
...overrides,
24+
});
25+
26+
const mockTransactionRepository = {
27+
findAll: jest.fn(),
28+
getLastUpdatedAt: jest.fn(),
29+
} as unknown as ITransactionRepository;
30+
31+
const mockPoliticalOrganizationRepository = {
32+
findBySlugs: jest.fn(),
33+
} as unknown as IPoliticalOrganizationRepository;
34+
35+
describe("GetAllTransactionsBySlugUsecase", () => {
36+
let usecase: GetAllTransactionsBySlugUsecase;
37+
38+
beforeEach(() => {
39+
jest.clearAllMocks();
40+
usecase = new GetAllTransactionsBySlugUsecase(
41+
mockTransactionRepository,
42+
mockPoliticalOrganizationRepository,
43+
);
44+
});
45+
46+
it("should return all transactions for valid organization", async () => {
47+
const mockOrganizations = [{ id: "1", slug: "test-org", displayName: "テスト組織" }];
48+
const mockTransactions = [
49+
createMockTransaction({ id: "1" }),
50+
createMockTransaction({ id: "2" }),
51+
createMockTransaction({ id: "3" }),
52+
];
53+
54+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
55+
mockOrganizations,
56+
);
57+
(mockTransactionRepository.findAll as jest.Mock).mockResolvedValue(mockTransactions);
58+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(
59+
new Date("2025-01-01"),
60+
);
61+
62+
const result = await usecase.execute({
63+
slugs: ["test-org"],
64+
financialYear: 2025,
65+
});
66+
67+
expect(result.transactions).toHaveLength(3);
68+
expect(result.total).toBe(3);
69+
expect(result.politicalOrganizations).toEqual(mockOrganizations);
70+
expect(result.lastUpdatedAt).toBe("2025-01-01T00:00:00.000Z");
71+
});
72+
73+
it("should throw error when organization not found", async () => {
74+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue([]);
75+
76+
await expect(
77+
usecase.execute({
78+
slugs: ["non-existent-org"],
79+
financialYear: 2025,
80+
}),
81+
).rejects.toThrow('Political organizations with slugs "non-existent-org" not found');
82+
});
83+
84+
it("should pass filter parameters correctly", async () => {
85+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
86+
87+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
88+
mockOrganizations,
89+
);
90+
(mockTransactionRepository.findAll as jest.Mock).mockResolvedValue([]);
91+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
92+
93+
const dateFrom = new Date("2025-01-01");
94+
const dateTo = new Date("2025-12-31");
95+
96+
await usecase.execute({
97+
slugs: ["test-org"],
98+
financialYear: 2025,
99+
transactionType: "income",
100+
dateFrom,
101+
dateTo,
102+
categories: ["donation-personal"],
103+
sortBy: "date",
104+
order: "desc",
105+
});
106+
107+
expect(mockTransactionRepository.findAll).toHaveBeenCalledWith(
108+
expect.objectContaining({
109+
political_organization_ids: ["1"],
110+
transaction_type: "income",
111+
date_from: dateFrom,
112+
date_to: dateTo,
113+
category_keys: ["donation-personal"],
114+
financial_year: 2025,
115+
}),
116+
expect.objectContaining({
117+
sortBy: "date",
118+
order: "desc",
119+
}),
120+
);
121+
});
122+
123+
it("should handle multiple organizations", async () => {
124+
const mockOrganizations = [
125+
{ id: "1", slug: "org-1" },
126+
{ id: "2", slug: "org-2" },
127+
];
128+
129+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
130+
mockOrganizations,
131+
);
132+
(mockTransactionRepository.findAll as jest.Mock).mockResolvedValue([]);
133+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
134+
135+
await usecase.execute({
136+
slugs: ["org-1", "org-2"],
137+
financialYear: 2025,
138+
});
139+
140+
expect(mockTransactionRepository.findAll).toHaveBeenCalledWith(
141+
expect.objectContaining({
142+
political_organization_ids: ["1", "2"],
143+
}),
144+
expect.any(Object),
145+
);
146+
});
147+
148+
it("should handle empty transactions", async () => {
149+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
150+
151+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
152+
mockOrganizations,
153+
);
154+
(mockTransactionRepository.findAll as jest.Mock).mockResolvedValue([]);
155+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
156+
157+
const result = await usecase.execute({
158+
slugs: ["test-org"],
159+
financialYear: 2025,
160+
});
161+
162+
expect(result.transactions).toEqual([]);
163+
expect(result.total).toBe(0);
164+
expect(result.lastUpdatedAt).toBeNull();
165+
});
166+
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { GetBalanceSheetUsecase } from "@/server/usecases/get-balance-sheet-usecase";
2+
import type { ITransactionRepository } from "@/server/repositories/interfaces/transaction-repository.interface";
3+
import type { IBalanceSnapshotRepository } from "@/server/repositories/interfaces/balance-snapshot-repository.interface";
4+
import type { IPoliticalOrganizationRepository } from "@/server/repositories/interfaces/political-organization-repository.interface";
5+
6+
const mockTransactionRepository = {
7+
getBorrowingIncomeTotal: jest.fn(),
8+
getBorrowingExpenseTotal: jest.fn(),
9+
getLiabilityBalance: jest.fn(),
10+
} as unknown as ITransactionRepository;
11+
12+
const mockBalanceSnapshotRepository = {
13+
getTotalLatestBalanceByOrgIds: jest.fn(),
14+
} as unknown as IBalanceSnapshotRepository;
15+
16+
const mockPoliticalOrganizationRepository = {
17+
findBySlugs: jest.fn(),
18+
} as unknown as IPoliticalOrganizationRepository;
19+
20+
describe("GetBalanceSheetUsecase", () => {
21+
let usecase: GetBalanceSheetUsecase;
22+
23+
beforeEach(() => {
24+
jest.clearAllMocks();
25+
usecase = new GetBalanceSheetUsecase(
26+
mockTransactionRepository,
27+
mockBalanceSnapshotRepository,
28+
mockPoliticalOrganizationRepository,
29+
);
30+
});
31+
32+
it("should return balance sheet data with positive net assets", async () => {
33+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
34+
35+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
36+
mockOrganizations,
37+
);
38+
(mockBalanceSnapshotRepository.getTotalLatestBalanceByOrgIds as jest.Mock).mockResolvedValue(
39+
1000000,
40+
);
41+
(mockTransactionRepository.getBorrowingIncomeTotal as jest.Mock).mockResolvedValue(500000);
42+
(mockTransactionRepository.getBorrowingExpenseTotal as jest.Mock).mockResolvedValue(200000);
43+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(100000);
44+
45+
const result = await usecase.execute({
46+
slugs: ["test-org"],
47+
financialYear: 2025,
48+
});
49+
50+
expect(result.balanceSheetData.left.currentAssets).toBe(1000000);
51+
expect(result.balanceSheetData.left.fixedAssets).toBe(0);
52+
expect(result.balanceSheetData.right.currentLiabilities).toBe(100000);
53+
expect(result.balanceSheetData.right.fixedLiabilities).toBe(300000);
54+
expect(result.balanceSheetData.right.netAssets).toBe(600000);
55+
expect(result.balanceSheetData.left.debtExcess).toBe(0);
56+
});
57+
58+
it("should return balance sheet data with debt excess", async () => {
59+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
60+
61+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
62+
mockOrganizations,
63+
);
64+
(mockBalanceSnapshotRepository.getTotalLatestBalanceByOrgIds as jest.Mock).mockResolvedValue(
65+
100000,
66+
);
67+
(mockTransactionRepository.getBorrowingIncomeTotal as jest.Mock).mockResolvedValue(1000000);
68+
(mockTransactionRepository.getBorrowingExpenseTotal as jest.Mock).mockResolvedValue(0);
69+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(200000);
70+
71+
const result = await usecase.execute({
72+
slugs: ["test-org"],
73+
financialYear: 2025,
74+
});
75+
76+
expect(result.balanceSheetData.left.currentAssets).toBe(100000);
77+
expect(result.balanceSheetData.left.fixedAssets).toBe(0);
78+
expect(result.balanceSheetData.right.currentLiabilities).toBe(200000);
79+
expect(result.balanceSheetData.right.fixedLiabilities).toBe(1000000);
80+
expect(result.balanceSheetData.right.netAssets).toBe(0);
81+
expect(result.balanceSheetData.left.debtExcess).toBe(1100000);
82+
});
83+
84+
it("should handle zero balance case", async () => {
85+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
86+
87+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
88+
mockOrganizations,
89+
);
90+
(mockBalanceSnapshotRepository.getTotalLatestBalanceByOrgIds as jest.Mock).mockResolvedValue(
91+
500000,
92+
);
93+
(mockTransactionRepository.getBorrowingIncomeTotal as jest.Mock).mockResolvedValue(500000);
94+
(mockTransactionRepository.getBorrowingExpenseTotal as jest.Mock).mockResolvedValue(0);
95+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(0);
96+
97+
const result = await usecase.execute({
98+
slugs: ["test-org"],
99+
financialYear: 2025,
100+
});
101+
102+
expect(result.balanceSheetData.right.netAssets).toBe(0);
103+
expect(result.balanceSheetData.left.debtExcess).toBe(0);
104+
});
105+
106+
it("should handle multiple organizations", async () => {
107+
const mockOrganizations = [
108+
{ id: "1", slug: "org-1" },
109+
{ id: "2", slug: "org-2" },
110+
];
111+
112+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
113+
mockOrganizations,
114+
);
115+
(mockBalanceSnapshotRepository.getTotalLatestBalanceByOrgIds as jest.Mock).mockResolvedValue(
116+
2000000,
117+
);
118+
(mockTransactionRepository.getBorrowingIncomeTotal as jest.Mock).mockResolvedValue(0);
119+
(mockTransactionRepository.getBorrowingExpenseTotal as jest.Mock).mockResolvedValue(0);
120+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(0);
121+
122+
const result = await usecase.execute({
123+
slugs: ["org-1", "org-2"],
124+
financialYear: 2025,
125+
});
126+
127+
expect(result.balanceSheetData.left.currentAssets).toBe(2000000);
128+
expect(mockBalanceSnapshotRepository.getTotalLatestBalanceByOrgIds).toHaveBeenCalledWith([
129+
"1",
130+
"2",
131+
]);
132+
});
133+
134+
it("should handle empty organization list gracefully", async () => {
135+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue([]);
136+
(mockBalanceSnapshotRepository.getTotalLatestBalanceByOrgIds as jest.Mock).mockResolvedValue(0);
137+
(mockTransactionRepository.getBorrowingIncomeTotal as jest.Mock).mockResolvedValue(0);
138+
(mockTransactionRepository.getBorrowingExpenseTotal as jest.Mock).mockResolvedValue(0);
139+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(0);
140+
141+
const result = await usecase.execute({
142+
slugs: ["non-existent-org"],
143+
financialYear: 2025,
144+
});
145+
146+
expect(result.balanceSheetData.left.currentAssets).toBe(0);
147+
expect(result.balanceSheetData.right.netAssets).toBe(0);
148+
});
149+
});
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { GetMonthlyTransactionAggregationUsecase } from "@/server/usecases/get-monthly-transaction-aggregation-usecase";
2+
import type {
3+
ITransactionRepository,
4+
MonthlyAggregation,
5+
} from "@/server/repositories/interfaces/transaction-repository.interface";
6+
import type { IPoliticalOrganizationRepository } from "@/server/repositories/interfaces/political-organization-repository.interface";
7+
8+
const mockTransactionRepository = {
9+
getMonthlyAggregation: jest.fn(),
10+
} as unknown as ITransactionRepository;
11+
12+
const mockPoliticalOrganizationRepository = {
13+
findBySlugs: jest.fn(),
14+
} as unknown as IPoliticalOrganizationRepository;
15+
16+
describe("GetMonthlyTransactionAggregationUsecase", () => {
17+
let usecase: GetMonthlyTransactionAggregationUsecase;
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
usecase = new GetMonthlyTransactionAggregationUsecase(
22+
mockTransactionRepository,
23+
mockPoliticalOrganizationRepository,
24+
);
25+
});
26+
27+
it("should return monthly aggregation data for valid organization", async () => {
28+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
29+
const mockMonthlyData: MonthlyAggregation[] = [
30+
{ yearMonth: "2025-01", income: 1000000, expense: 500000 },
31+
{ yearMonth: "2025-02", income: 800000, expense: 600000 },
32+
{ yearMonth: "2025-03", income: 1200000, expense: 400000 },
33+
];
34+
35+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
36+
mockOrganizations,
37+
);
38+
(mockTransactionRepository.getMonthlyAggregation as jest.Mock).mockResolvedValue(
39+
mockMonthlyData,
40+
);
41+
42+
const result = await usecase.execute({
43+
slugs: ["test-org"],
44+
financialYear: 2025,
45+
});
46+
47+
expect(result.monthlyData).toHaveLength(3);
48+
expect(result.monthlyData[0]).toEqual({
49+
yearMonth: "2025-01",
50+
income: 1000000,
51+
expense: 500000,
52+
});
53+
expect(mockPoliticalOrganizationRepository.findBySlugs).toHaveBeenCalledWith(["test-org"]);
54+
expect(mockTransactionRepository.getMonthlyAggregation).toHaveBeenCalledWith(["1"], 2025);
55+
});
56+
57+
it("should throw error when organization not found", async () => {
58+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue([]);
59+
60+
await expect(
61+
usecase.execute({
62+
slugs: ["non-existent-org"],
63+
financialYear: 2025,
64+
}),
65+
).rejects.toThrow('Political organizations with slugs "non-existent-org" not found');
66+
});
67+
68+
it("should handle multiple organizations", async () => {
69+
const mockOrganizations = [
70+
{ id: "1", slug: "org-1" },
71+
{ id: "2", slug: "org-2" },
72+
];
73+
const mockMonthlyData: MonthlyAggregation[] = [
74+
{ yearMonth: "2025-01", income: 2000000, expense: 1000000 },
75+
];
76+
77+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
78+
mockOrganizations,
79+
);
80+
(mockTransactionRepository.getMonthlyAggregation as jest.Mock).mockResolvedValue(
81+
mockMonthlyData,
82+
);
83+
84+
const result = await usecase.execute({
85+
slugs: ["org-1", "org-2"],
86+
financialYear: 2025,
87+
});
88+
89+
expect(result.monthlyData).toHaveLength(1);
90+
expect(mockTransactionRepository.getMonthlyAggregation).toHaveBeenCalledWith(
91+
["1", "2"],
92+
2025,
93+
);
94+
});
95+
96+
it("should handle empty monthly data", async () => {
97+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
98+
99+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
100+
mockOrganizations,
101+
);
102+
(mockTransactionRepository.getMonthlyAggregation as jest.Mock).mockResolvedValue([]);
103+
104+
const result = await usecase.execute({
105+
slugs: ["test-org"],
106+
financialYear: 2025,
107+
});
108+
109+
expect(result.monthlyData).toEqual([]);
110+
});
111+
112+
it("should handle different financial years", async () => {
113+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
114+
const mockMonthlyData: MonthlyAggregation[] = [
115+
{ yearMonth: "2024-04", income: 500000, expense: 300000 },
116+
];
117+
118+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
119+
mockOrganizations,
120+
);
121+
(mockTransactionRepository.getMonthlyAggregation as jest.Mock).mockResolvedValue(
122+
mockMonthlyData,
123+
);
124+
125+
const result = await usecase.execute({
126+
slugs: ["test-org"],
127+
financialYear: 2024,
128+
});
129+
130+
expect(result.monthlyData).toHaveLength(1);
131+
expect(mockTransactionRepository.getMonthlyAggregation).toHaveBeenCalledWith(["1"], 2024);
132+
});
133+
});
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { GetSankeyAggregationUsecase } from "@/server/usecases/get-sankey-aggregation-usecase";
2+
import type {
3+
ITransactionRepository,
4+
SankeyCategoryAggregationResult,
5+
} from "@/server/repositories/interfaces/transaction-repository.interface";
6+
import type { IPoliticalOrganizationRepository } from "@/server/repositories/interfaces/political-organization-repository.interface";
7+
import type {
8+
IBalanceSnapshotRepository,
9+
TotalBalancesByYear,
10+
} from "@/server/repositories/interfaces/balance-snapshot-repository.interface";
11+
12+
const mockTransactionRepository = {
13+
getCategoryAggregationForSankey: jest.fn(),
14+
getLiabilityBalance: jest.fn(),
15+
} as unknown as ITransactionRepository;
16+
17+
const mockPoliticalOrganizationRepository = {
18+
findBySlugs: jest.fn(),
19+
} as unknown as IPoliticalOrganizationRepository;
20+
21+
const mockBalanceSnapshotRepository = {
22+
getTotalLatestBalancesByYear: jest.fn(),
23+
} as unknown as IBalanceSnapshotRepository;
24+
25+
describe("GetSankeyAggregationUsecase", () => {
26+
let usecase: GetSankeyAggregationUsecase;
27+
28+
beforeEach(() => {
29+
jest.clearAllMocks();
30+
usecase = new GetSankeyAggregationUsecase(
31+
mockTransactionRepository,
32+
mockPoliticalOrganizationRepository,
33+
mockBalanceSnapshotRepository,
34+
);
35+
});
36+
37+
it("should return sankey data for valid organization", async () => {
38+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
39+
const mockAggregation: SankeyCategoryAggregationResult = {
40+
income: [
41+
{
42+
category: "寄附",
43+
subcategory: "個人からの寄附",
44+
totalAmount: 1000000,
45+
},
46+
],
47+
expense: [
48+
{
49+
category: "政治活動費",
50+
subcategory: "宣伝費",
51+
totalAmount: 500000,
52+
},
53+
],
54+
};
55+
const mockBalances: TotalBalancesByYear = {
56+
currentYear: 300000,
57+
previousYear: 200000,
58+
};
59+
60+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
61+
mockOrganizations,
62+
);
63+
(mockTransactionRepository.getCategoryAggregationForSankey as jest.Mock).mockResolvedValue(
64+
mockAggregation,
65+
);
66+
(mockBalanceSnapshotRepository.getTotalLatestBalancesByYear as jest.Mock).mockResolvedValue(
67+
mockBalances,
68+
);
69+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(0);
70+
71+
const result = await usecase.execute({
72+
slugs: ["test-org"],
73+
financialYear: 2025,
74+
categoryType: "political-category",
75+
});
76+
77+
expect(result.sankeyData).toBeDefined();
78+
expect(result.sankeyData.nodes).toBeDefined();
79+
expect(result.sankeyData.links).toBeDefined();
80+
expect(mockPoliticalOrganizationRepository.findBySlugs).toHaveBeenCalledWith(["test-org"]);
81+
expect(mockTransactionRepository.getCategoryAggregationForSankey).toHaveBeenCalledWith(
82+
["1"],
83+
2025,
84+
"political-category",
85+
);
86+
});
87+
88+
it("should handle friendly-category type", async () => {
89+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
90+
const mockAggregation: SankeyCategoryAggregationResult = {
91+
income: [{ category: "寄附", totalAmount: 1000000 }],
92+
expense: [{ category: "政治活動費", totalAmount: 500000 }],
93+
};
94+
const mockBalances: TotalBalancesByYear = {
95+
currentYear: 0,
96+
previousYear: 0,
97+
};
98+
99+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
100+
mockOrganizations,
101+
);
102+
(mockTransactionRepository.getCategoryAggregationForSankey as jest.Mock).mockResolvedValue(
103+
mockAggregation,
104+
);
105+
(mockBalanceSnapshotRepository.getTotalLatestBalancesByYear as jest.Mock).mockResolvedValue(
106+
mockBalances,
107+
);
108+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(0);
109+
110+
const result = await usecase.execute({
111+
slugs: ["test-org"],
112+
financialYear: 2025,
113+
categoryType: "friendly-category",
114+
});
115+
116+
expect(result.sankeyData).toBeDefined();
117+
expect(mockTransactionRepository.getCategoryAggregationForSankey).toHaveBeenCalledWith(
118+
["1"],
119+
2025,
120+
"friendly-category",
121+
);
122+
});
123+
124+
it("should throw error when organization not found", async () => {
125+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue([]);
126+
127+
await expect(
128+
usecase.execute({
129+
slugs: ["non-existent-org"],
130+
financialYear: 2025,
131+
}),
132+
).rejects.toThrow('Political organizations with slugs "non-existent-org" not found');
133+
});
134+
135+
it("should handle multiple organizations", async () => {
136+
const mockOrganizations = [
137+
{ id: "1", slug: "org-1" },
138+
{ id: "2", slug: "org-2" },
139+
];
140+
const mockAggregation: SankeyCategoryAggregationResult = {
141+
income: [{ category: "寄附", totalAmount: 2000000 }],
142+
expense: [{ category: "政治活動費", totalAmount: 1000000 }],
143+
};
144+
const mockBalances: TotalBalancesByYear = {
145+
currentYear: 500000,
146+
previousYear: 300000,
147+
};
148+
149+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
150+
mockOrganizations,
151+
);
152+
(mockTransactionRepository.getCategoryAggregationForSankey as jest.Mock).mockResolvedValue(
153+
mockAggregation,
154+
);
155+
(mockBalanceSnapshotRepository.getTotalLatestBalancesByYear as jest.Mock).mockResolvedValue(
156+
mockBalances,
157+
);
158+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(0);
159+
160+
const result = await usecase.execute({
161+
slugs: ["org-1", "org-2"],
162+
financialYear: 2025,
163+
});
164+
165+
expect(result.sankeyData).toBeDefined();
166+
expect(mockTransactionRepository.getCategoryAggregationForSankey).toHaveBeenCalledWith(
167+
["1", "2"],
168+
2025,
169+
undefined,
170+
);
171+
});
172+
173+
it("should handle empty aggregation data", async () => {
174+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
175+
const mockAggregation: SankeyCategoryAggregationResult = {
176+
income: [],
177+
expense: [],
178+
};
179+
const mockBalances: TotalBalancesByYear = {
180+
currentYear: 0,
181+
previousYear: 0,
182+
};
183+
184+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
185+
mockOrganizations,
186+
);
187+
(mockTransactionRepository.getCategoryAggregationForSankey as jest.Mock).mockResolvedValue(
188+
mockAggregation,
189+
);
190+
(mockBalanceSnapshotRepository.getTotalLatestBalancesByYear as jest.Mock).mockResolvedValue(
191+
mockBalances,
192+
);
193+
(mockTransactionRepository.getLiabilityBalance as jest.Mock).mockResolvedValue(0);
194+
195+
const result = await usecase.execute({
196+
slugs: ["test-org"],
197+
financialYear: 2025,
198+
});
199+
200+
expect(result.sankeyData).toBeDefined();
201+
expect(result.sankeyData.nodes).toHaveLength(1);
202+
expect(result.sankeyData.nodes[0].label).toBe("合計");
203+
});
204+
});
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { GetTransactionsBySlugUsecase } from "@/server/usecases/get-transactions-by-slug-usecase";
2+
import type {
3+
ITransactionRepository,
4+
PaginatedResult,
5+
} from "@/server/repositories/interfaces/transaction-repository.interface";
6+
import type { IPoliticalOrganizationRepository } from "@/server/repositories/interfaces/political-organization-repository.interface";
7+
import type { Transaction } from "@/shared/models/transaction";
8+
9+
const createMockTransaction = (overrides: Partial<Transaction> = {}): Transaction => ({
10+
id: "test-id-1",
11+
political_organization_id: "org-1",
12+
transaction_no: "T001",
13+
transaction_date: new Date("2025-08-15"),
14+
financial_year: 2025,
15+
transaction_type: "expense",
16+
debit_account: "政治活動費",
17+
debit_amount: 100000,
18+
credit_account: "現金",
19+
credit_amount: 100000,
20+
friendly_category: "支出",
21+
category_key: "political-activity",
22+
label: "テスト取引",
23+
hash: "test-hash",
24+
created_at: new Date(),
25+
updated_at: new Date(),
26+
...overrides,
27+
});
28+
29+
const mockTransactionRepository = {
30+
findWithPagination: jest.fn(),
31+
getLastUpdatedAt: jest.fn(),
32+
} as unknown as ITransactionRepository;
33+
34+
const mockPoliticalOrganizationRepository = {
35+
findBySlugs: jest.fn(),
36+
} as unknown as IPoliticalOrganizationRepository;
37+
38+
describe("GetTransactionsBySlugUsecase", () => {
39+
let usecase: GetTransactionsBySlugUsecase;
40+
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
usecase = new GetTransactionsBySlugUsecase(
44+
mockTransactionRepository,
45+
mockPoliticalOrganizationRepository,
46+
);
47+
});
48+
49+
it("should return paginated transactions for valid organization", async () => {
50+
const mockOrganizations = [{ id: "1", slug: "test-org", displayName: "テスト組織" }];
51+
const mockTransactions = [
52+
createMockTransaction({ id: "1" }),
53+
createMockTransaction({ id: "2" }),
54+
];
55+
const mockPaginatedResult: PaginatedResult<Transaction> = {
56+
items: mockTransactions,
57+
total: 2,
58+
page: 1,
59+
perPage: 50,
60+
totalPages: 1,
61+
};
62+
63+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
64+
mockOrganizations,
65+
);
66+
(mockTransactionRepository.findWithPagination as jest.Mock).mockResolvedValue(
67+
mockPaginatedResult,
68+
);
69+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(
70+
new Date("2025-01-01"),
71+
);
72+
73+
const result = await usecase.execute({
74+
slugs: ["test-org"],
75+
financialYear: 2025,
76+
});
77+
78+
expect(result.transactions).toHaveLength(2);
79+
expect(result.total).toBe(2);
80+
expect(result.page).toBe(1);
81+
expect(result.perPage).toBe(50);
82+
expect(result.totalPages).toBe(1);
83+
expect(result.politicalOrganizations).toEqual(mockOrganizations);
84+
expect(result.lastUpdatedAt).toBe("2025-01-01T00:00:00.000Z");
85+
});
86+
87+
it("should throw error when organization not found", async () => {
88+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue([]);
89+
90+
await expect(
91+
usecase.execute({
92+
slugs: ["non-existent-org"],
93+
financialYear: 2025,
94+
}),
95+
).rejects.toThrow('Political organizations with slugs "non-existent-org" not found');
96+
});
97+
98+
it("should handle pagination parameters", async () => {
99+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
100+
const mockPaginatedResult: PaginatedResult<Transaction> = {
101+
items: [createMockTransaction()],
102+
total: 100,
103+
page: 2,
104+
perPage: 10,
105+
totalPages: 10,
106+
};
107+
108+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
109+
mockOrganizations,
110+
);
111+
(mockTransactionRepository.findWithPagination as jest.Mock).mockResolvedValue(
112+
mockPaginatedResult,
113+
);
114+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
115+
116+
const result = await usecase.execute({
117+
slugs: ["test-org"],
118+
financialYear: 2025,
119+
page: 2,
120+
perPage: 10,
121+
});
122+
123+
expect(result.page).toBe(2);
124+
expect(result.perPage).toBe(10);
125+
expect(result.totalPages).toBe(10);
126+
expect(result.lastUpdatedAt).toBeNull();
127+
});
128+
129+
it("should enforce perPage maximum of 100", async () => {
130+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
131+
const mockPaginatedResult: PaginatedResult<Transaction> = {
132+
items: [],
133+
total: 0,
134+
page: 1,
135+
perPage: 100,
136+
totalPages: 0,
137+
};
138+
139+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
140+
mockOrganizations,
141+
);
142+
(mockTransactionRepository.findWithPagination as jest.Mock).mockResolvedValue(
143+
mockPaginatedResult,
144+
);
145+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
146+
147+
await usecase.execute({
148+
slugs: ["test-org"],
149+
financialYear: 2025,
150+
perPage: 200,
151+
});
152+
153+
expect(mockTransactionRepository.findWithPagination).toHaveBeenCalledWith(
154+
expect.any(Object),
155+
expect.objectContaining({ perPage: 100 }),
156+
);
157+
});
158+
159+
it("should enforce page minimum of 1", async () => {
160+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
161+
const mockPaginatedResult: PaginatedResult<Transaction> = {
162+
items: [],
163+
total: 0,
164+
page: 1,
165+
perPage: 50,
166+
totalPages: 0,
167+
};
168+
169+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
170+
mockOrganizations,
171+
);
172+
(mockTransactionRepository.findWithPagination as jest.Mock).mockResolvedValue(
173+
mockPaginatedResult,
174+
);
175+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
176+
177+
await usecase.execute({
178+
slugs: ["test-org"],
179+
financialYear: 2025,
180+
page: -1,
181+
});
182+
183+
expect(mockTransactionRepository.findWithPagination).toHaveBeenCalledWith(
184+
expect.any(Object),
185+
expect.objectContaining({ page: 1 }),
186+
);
187+
});
188+
189+
it("should pass filter parameters correctly", async () => {
190+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
191+
const mockPaginatedResult: PaginatedResult<Transaction> = {
192+
items: [],
193+
total: 0,
194+
page: 1,
195+
perPage: 50,
196+
totalPages: 0,
197+
};
198+
199+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
200+
mockOrganizations,
201+
);
202+
(mockTransactionRepository.findWithPagination as jest.Mock).mockResolvedValue(
203+
mockPaginatedResult,
204+
);
205+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
206+
207+
const dateFrom = new Date("2025-01-01");
208+
const dateTo = new Date("2025-12-31");
209+
210+
await usecase.execute({
211+
slugs: ["test-org"],
212+
financialYear: 2025,
213+
transactionType: "income",
214+
dateFrom,
215+
dateTo,
216+
categories: ["donation-personal"],
217+
sortBy: "date",
218+
order: "desc",
219+
});
220+
221+
expect(mockTransactionRepository.findWithPagination).toHaveBeenCalledWith(
222+
expect.objectContaining({
223+
political_organization_ids: ["1"],
224+
transaction_type: "income",
225+
date_from: dateFrom,
226+
date_to: dateTo,
227+
category_keys: ["donation-personal"],
228+
financial_year: 2025,
229+
}),
230+
expect.objectContaining({
231+
sortBy: "date",
232+
order: "desc",
233+
}),
234+
);
235+
});
236+
237+
it("should handle multiple organizations", async () => {
238+
const mockOrganizations = [
239+
{ id: "1", slug: "org-1" },
240+
{ id: "2", slug: "org-2" },
241+
];
242+
const mockPaginatedResult: PaginatedResult<Transaction> = {
243+
items: [],
244+
total: 0,
245+
page: 1,
246+
perPage: 50,
247+
totalPages: 0,
248+
};
249+
250+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
251+
mockOrganizations,
252+
);
253+
(mockTransactionRepository.findWithPagination as jest.Mock).mockResolvedValue(
254+
mockPaginatedResult,
255+
);
256+
(mockTransactionRepository.getLastUpdatedAt as jest.Mock).mockResolvedValue(null);
257+
258+
await usecase.execute({
259+
slugs: ["org-1", "org-2"],
260+
financialYear: 2025,
261+
});
262+
263+
expect(mockTransactionRepository.findWithPagination).toHaveBeenCalledWith(
264+
expect.objectContaining({
265+
political_organization_ids: ["1", "2"],
266+
}),
267+
expect.any(Object),
268+
);
269+
});
270+
});
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { GetTransactionsForCsvUsecase } from "@/server/usecases/get-transactions-for-csv-usecase";
2+
import type { ITransactionRepository } from "@/server/repositories/interfaces/transaction-repository.interface";
3+
import type { IPoliticalOrganizationRepository } from "@/server/repositories/interfaces/political-organization-repository.interface";
4+
import type { Transaction } from "@/shared/models/transaction";
5+
6+
const createMockTransactionWithOrgName = (
7+
overrides: Partial<Transaction & { political_organization_name: string }> = {},
8+
): Transaction & { political_organization_name: string } => ({
9+
id: "test-id-1",
10+
political_organization_id: "org-1",
11+
political_organization_name: "テスト組織",
12+
transaction_no: "T001",
13+
transaction_date: new Date("2025-08-15"),
14+
financial_year: 2025,
15+
transaction_type: "expense",
16+
debit_account: "政治活動費",
17+
debit_amount: 100000,
18+
credit_account: "現金",
19+
credit_amount: 100000,
20+
friendly_category: "支出",
21+
category_key: "political-activity",
22+
label: "テスト取引",
23+
hash: "test-hash",
24+
created_at: new Date(),
25+
updated_at: new Date(),
26+
...overrides,
27+
});
28+
29+
const mockTransactionRepository = {
30+
findAllWithPoliticalOrganizationName: jest.fn(),
31+
} as unknown as ITransactionRepository;
32+
33+
const mockPoliticalOrganizationRepository = {
34+
findBySlugs: jest.fn(),
35+
} as unknown as IPoliticalOrganizationRepository;
36+
37+
describe("GetTransactionsForCsvUsecase", () => {
38+
let usecase: GetTransactionsForCsvUsecase;
39+
40+
beforeEach(() => {
41+
jest.clearAllMocks();
42+
usecase = new GetTransactionsForCsvUsecase(
43+
mockTransactionRepository,
44+
mockPoliticalOrganizationRepository,
45+
);
46+
});
47+
48+
it("should return transactions with organization name for valid organization", async () => {
49+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
50+
const mockTransactions = [
51+
createMockTransactionWithOrgName({ id: "1", political_organization_name: "組織A" }),
52+
createMockTransactionWithOrgName({ id: "2", political_organization_name: "組織A" }),
53+
];
54+
55+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
56+
mockOrganizations,
57+
);
58+
(
59+
mockTransactionRepository.findAllWithPoliticalOrganizationName as jest.Mock
60+
).mockResolvedValue(mockTransactions);
61+
62+
const result = await usecase.execute({
63+
slugs: ["test-org"],
64+
financialYear: 2025,
65+
});
66+
67+
expect(result.transactions).toHaveLength(2);
68+
expect(result.total).toBe(2);
69+
expect(result.transactions[0].political_organization_name).toBe("組織A");
70+
});
71+
72+
it("should throw error when organization not found", async () => {
73+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue([]);
74+
75+
await expect(
76+
usecase.execute({
77+
slugs: ["non-existent-org"],
78+
financialYear: 2025,
79+
}),
80+
).rejects.toThrow('Political organizations with slugs "non-existent-org" not found');
81+
});
82+
83+
it("should pass correct filters to repository", async () => {
84+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
85+
86+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
87+
mockOrganizations,
88+
);
89+
(
90+
mockTransactionRepository.findAllWithPoliticalOrganizationName as jest.Mock
91+
).mockResolvedValue([]);
92+
93+
await usecase.execute({
94+
slugs: ["test-org"],
95+
financialYear: 2025,
96+
});
97+
98+
expect(mockTransactionRepository.findAllWithPoliticalOrganizationName).toHaveBeenCalledWith({
99+
political_organization_ids: ["1"],
100+
financial_year: 2025,
101+
});
102+
});
103+
104+
it("should handle multiple organizations", async () => {
105+
const mockOrganizations = [
106+
{ id: "1", slug: "org-1" },
107+
{ id: "2", slug: "org-2" },
108+
];
109+
const mockTransactions = [
110+
createMockTransactionWithOrgName({ id: "1", political_organization_name: "組織A" }),
111+
createMockTransactionWithOrgName({ id: "2", political_organization_name: "組織B" }),
112+
];
113+
114+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
115+
mockOrganizations,
116+
);
117+
(
118+
mockTransactionRepository.findAllWithPoliticalOrganizationName as jest.Mock
119+
).mockResolvedValue(mockTransactions);
120+
121+
const result = await usecase.execute({
122+
slugs: ["org-1", "org-2"],
123+
financialYear: 2025,
124+
});
125+
126+
expect(result.transactions).toHaveLength(2);
127+
expect(mockTransactionRepository.findAllWithPoliticalOrganizationName).toHaveBeenCalledWith({
128+
political_organization_ids: ["1", "2"],
129+
financial_year: 2025,
130+
});
131+
});
132+
133+
it("should handle empty transactions", async () => {
134+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
135+
136+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
137+
mockOrganizations,
138+
);
139+
(
140+
mockTransactionRepository.findAllWithPoliticalOrganizationName as jest.Mock
141+
).mockResolvedValue([]);
142+
143+
const result = await usecase.execute({
144+
slugs: ["test-org"],
145+
financialYear: 2025,
146+
});
147+
148+
expect(result.transactions).toEqual([]);
149+
expect(result.total).toBe(0);
150+
});
151+
152+
it("should handle different financial years", async () => {
153+
const mockOrganizations = [{ id: "1", slug: "test-org" }];
154+
155+
(mockPoliticalOrganizationRepository.findBySlugs as jest.Mock).mockResolvedValue(
156+
mockOrganizations,
157+
);
158+
(
159+
mockTransactionRepository.findAllWithPoliticalOrganizationName as jest.Mock
160+
).mockResolvedValue([]);
161+
162+
await usecase.execute({
163+
slugs: ["test-org"],
164+
financialYear: 2024,
165+
});
166+
167+
expect(mockTransactionRepository.findAllWithPoliticalOrganizationName).toHaveBeenCalledWith({
168+
political_organization_ids: ["1"],
169+
financial_year: 2024,
170+
});
171+
});
172+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createSafariCompatibleId } from "@/server/utils/sankey-id-utils";
2+
3+
describe("createSafariCompatibleId", () => {
4+
it("should keep alphanumeric characters unchanged", () => {
5+
expect(createSafariCompatibleId("abc123")).toBe("abc123");
6+
expect(createSafariCompatibleId("ABC")).toBe("ABC");
7+
expect(createSafariCompatibleId("test-id")).toBe("test-id");
8+
expect(createSafariCompatibleId("test_id")).toBe("test_id");
9+
});
10+
11+
it("should convert Japanese characters to hash", () => {
12+
const result = createSafariCompatibleId("寄附");
13+
expect(result).not.toBe("寄附");
14+
expect(result).toMatch(/^_[a-z0-9]+_[a-z0-9]+$/);
15+
});
16+
17+
it("should convert spaces to hash", () => {
18+
const result = createSafariCompatibleId("test id");
19+
expect(result).not.toBe("test id");
20+
expect(result).toContain("test");
21+
expect(result).toContain("id");
22+
expect(result).toMatch(/_[a-z0-9]+/);
23+
});
24+
25+
it("should handle mixed content", () => {
26+
const result = createSafariCompatibleId("income-寄附-123");
27+
expect(result).toContain("income-");
28+
expect(result).toContain("-123");
29+
expect(result).not.toContain("寄附");
30+
});
31+
32+
it("should produce consistent results for same input", () => {
33+
const input = "政治活動費";
34+
const result1 = createSafariCompatibleId(input);
35+
const result2 = createSafariCompatibleId(input);
36+
expect(result1).toBe(result2);
37+
});
38+
39+
it("should handle empty string", () => {
40+
expect(createSafariCompatibleId("")).toBe("");
41+
});
42+
43+
it("should handle special characters", () => {
44+
const result = createSafariCompatibleId("test@#$%");
45+
expect(result).toContain("test");
46+
expect(result).not.toContain("@");
47+
expect(result).not.toContain("#");
48+
expect(result).not.toContain("$");
49+
expect(result).not.toContain("%");
50+
});
51+
52+
it("should produce different hashes for different characters", () => {
53+
const result1 = createSafariCompatibleId("あ");
54+
const result2 = createSafariCompatibleId("い");
55+
expect(result1).not.toBe(result2);
56+
});
57+
});

0 commit comments

Comments
 (0)
Please sign in to comment.