Skip to content

Add portfolio asset management and display features#1148

Closed
osahiroshima wants to merge 2 commits intoteam-mirai:developfrom
osahiroshima:claude/upload-template-docs-Ku41N
Closed

Add portfolio asset management and display features#1148
osahiroshima wants to merge 2 commits intoteam-mirai:developfrom
osahiroshima:claude/upload-template-docs-Ku41N

Conversation

@osahiroshima
Copy link

@osahiroshima osahiroshima commented Mar 8, 2026

Summary

This PR adds comprehensive portfolio asset management capabilities to the system, including CSV import functionality in the admin panel and a new portfolio visualization component in the public webapp.

Key Changes

Admin Panel - Portfolio CSV Import

  • Added PortfolioCsvImportClient component for uploading portfolio asset data via CSV
  • Implemented PortfolioCsvLoader to parse and validate CSV files with support for 4 required columns (category, label, amount, snapshotDate)
  • Created ImportPortfolioCsvUsecase with validation for asset categories (cash, stocks, precious_metals, real_estate, other) and date formats
  • Added server action importPortfolioCsv to handle CSV processing and database persistence
  • Implemented automatic data replacement: same-date portfolio data is fully replaced on re-import (not incremental)
  • Added new /import-portfolio admin page for portfolio data management
  • Updated admin sidebar navigation to include portfolio import link

Webapp - Portfolio Display

  • Created PortfolioSection component to display portfolio data on the public site
  • Implemented PortfolioPieChart component using ApexCharts for donut chart visualization with:
    • Category-based color coding
    • Japanese number formatting (万円/億円 notation)
    • Responsive design with loading state
  • Added GetPortfolioUsecase to retrieve latest portfolio snapshot data
  • Implemented PrismaPortfolioRepository to fetch portfolio assets from database
  • Created PortfolioData domain model for type-safe data handling

Database

  • Added PortfolioAsset table to Prisma schema with fields for organization, snapshot date, category, label, and amount
  • Created migration to establish portfolio_assets table with appropriate indexes

Documentation

  • Added comprehensive upload-guide.md (403 lines) documenting:
    • Complete data upload workflow and operational procedures
    • CSV specifications for transactions, donors, and portfolio data
    • Troubleshooting guide and template file references
    • Monthly and annual update procedures

Templates

  • Added mf-transactions-template.csv for MF Cloud transaction imports
  • Added donor-template.csv for donor information bulk import
  • Added portfolio-template.csv for portfolio asset data

Other Updates

  • Updated webapp metadata to use "運営資金" (operational funds) instead of "政治資金" (political funds)
  • Added force-dynamic export to webapp layout for real-time data
  • Updated build scripts for Vercel deployment in both admin and webapp packages
  • Added test API endpoint for dynamic route verification
  • Enhanced cache revalidation with tags for organizations data

Implementation Details

  • Portfolio data uses BigInt for amounts to handle large financial values
  • CSV parsing handles both quoted and unquoted fields with proper escape sequence handling
  • Same-date portfolio data replacement ensures data consistency (deletes all assets for a date before inserting new ones)
  • Maximum 100 rows per portfolio CSV import to prevent abuse
  • Supports both Shift_JIS and UTF-8 CSV encodings in transaction imports

https://claude.ai/code/session_01Wo1xtz4a6cPvFg43G5Y7fy

Summary by CodeRabbit

  • ドキュメント
    • 管理UIのデータアップロード操作に関する包括的なガイドを追加しました。トランザクションデータ、ドナー情報、ポートフォリオ資産のインポート方法、定期更新ワークフロー、トラブルシューティング手順、およびテンプレートファイル一覧を記載しています。

@vercel
Copy link
Contributor

vercel bot commented Mar 8, 2026

@claude is attempting to deploy a commit to the team-mirai Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

ウォークスルー

管理UI内のデータアップロード操作を詳細に記載した新しいドキュメントファイルを追加。トランザクションデータ、寄付者情報、ポートフォリオ資産のインポート方法、定期更新ワークフロー、トラブルシューティング、テンプレートファイル一覧をカバーしています。

変更内容

Cohort / File(s) 概要
ドキュメント追加
docs/upload-guide.md
管理画面のアップロード機能に関するドキュメントを新規作成。MFクラウドCSVのトランザクション取込、Donor CSVでの寄付者一括登録、Portfolio CSVでの資産インポート、月次・年次の定期更新ワークフロー、トラブルシューティング、テンプレートファイル一覧を記載。CSV仕様、自動マッピングルール、重複検出、エンコーディング処理、管理者ルート・テンプレート情報も含む。

推定レビュー労力

🎯 3 (Moderate) | ⏱️ ~20分

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning プルリクエストのタイトル「Add portfolio asset management and display features」は、変更内容のまとめドキュメント(upload-guide.md)の追加という実際の変更と一致していません。タイトルはポートフォリオ資産管理機能を示唆していますが、このPRの実際の変更は、ドキュメント、テンプレート、インフラ設定の更新が中心です。 タイトルを「Add portfolio asset management documentation and templates」または「Document data upload operations and add CSV templates」など、実際の変更内容(ドキュメントとテンプレートの追加)をより正確に反映したものに変更してください。
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@admin/src/client/components/portfolio-csv-import/PortfolioCsvImportClient.tsx`:
- Around line 118-123: Replace the raw <button> in PortfolioCsvImportClient (the
element using onClick={handleImport}, disabled={!file ||
!politicalOrganizationId || isImporting}, and the className string) with the
shadcn Button exported from "@/client/components/ui"; import { Button } from
"@/client/components/ui" at the top, then use <Button type="button"
onClick={handleImport} disabled={!file || !politicalOrganizationId ||
isImporting}> and map styling to Button props (e.g., variant/size) or keep
className if needed, ensuring the disabled and onClick behavior and isImporting
state handling remain unchanged.
- Around line 61-66: File.text() を使うとブラウザ側で常に UTF-8 として文字列化され Shift_JIS
のバイト列が壊れるため、PortfolioCsvImportClient.tsx 内の importActionRef.current 呼び出しに渡す
csvContent を file.text() で作る実装をやめてください。代わりに file.arrayBuffer()
で生のバイト列を取得してサーバへ送るか、FormData に file をそのまま append して送信するように変更する——またはブラウザ側で対応するなら
TextDecoder('shift_jis') を使して適切にデコードしてから importActionRef.current に渡すように修正してください。

In
`@admin/src/server/contexts/data-import/application/usecases/import-portfolio-csv-usecase.ts`:
- Around line 43-46: 現在の validation は正規表現で YYYY-MM-DD 形式のみを確認しており `2024-02-31`
のような実在しない日を通してしまうため、record.snapshotDate の実在日チェックを追加してください:
import-portfolio-csv-usecase.ts の処理で record.snapshotDate を YYYY-MM-DD
で分割して年・月・日を数値化し、new Date(year, month-1, day) を生成してから date.getFullYear() /
date.getMonth()+1 / date.getDate() が元の値と一致するか(および生成した日付が有効か)を検証し、不一致や無効なら現在の
throw new Error(...) を用いて拒否するように修正してください。なお既存の /^\d{4}-\d{2}-\d{2}$/
チェックは残して構造チェック→実在日チェックの順で行ってください。
- Around line 55-74: 現在の削除(snapshotDates をループして
this.prisma.portfolioAsset.deleteMany
を呼ぶ)と挿入(this.prisma.portfolioAsset.createMany)が別トランザクションで実行されており途中で失敗すると既存データが失われるので、これらを単一の
DB トランザクションにまとめて原子的に実行するよう修正してください;具体的には snapshotDates の deleteMany 呼び出し群と
records を使った createMany を this.prisma.$transaction 内で実行する(もしくは tx
を受け取るトランザクションコールバック内で tx.portfolioAsset.deleteMany(...) をループしてから
tx.portfolioAsset.createMany(...) を呼ぶ)ことで、delete と create
を一括でロールバックできるようにしてください(参照箇所: snapshotDates, records,
this.prisma.portfolioAsset.deleteMany, this.prisma.portfolioAsset.createMany)。
- Around line 3-21: ImportPortfolioCsvUsecase is directly depending on
infrastructure types (PrismaClient and PortfolioCsvLoader); change it to depend
on application-layer abstractions instead: define minimal interfaces in the
application layer such as PortfolioRepository (methods used to persist
portfolios) and CsvLoader (methods to parse CSV) and replace the constructor
signature of ImportPortfolioCsvUsecase to accept these interfaces rather than
PrismaClient or the infrastructure PortfolioCsvLoader; remove the PrismaClient
import and the infra import from this file, and update the composition
root/presentation layer to wire the concrete Prisma-based repository and
PortfolioCsvLoader implementation to those interfaces.

In
`@admin/src/server/contexts/data-import/infrastructure/portfolio-csv/portfolio-csv-loader.ts`:
- Around line 26-35: The header-parsing code treats a UTF-8 BOM as part of the
first header, causing REQUIRED_COLUMNS checks to fail; before calling
this.parseCSVLine(headerLine) (i.e. where headerLine is read), strip any leading
BOM (U+FEFF) and trim surrounding whitespace from headerLine so the first column
becomes "category" not "\uFEFFcategory", then proceed with parseCSVLine and the
existing REQUIRED_COLUMNS validation.

In
`@admin/src/server/contexts/data-import/presentation/actions/import-portfolio-csv.ts`:
- Around line 36-40: The action currently only calls
revalidatePath("/import-portfolio") after usecase.execute, so public pages that
read PortfolioAsset remain stale; update the action (after usecase.execute and
before returning the result) to also revalidate the public routes that render
PortfolioAsset—e.g. revalidatePath("/") (public top) and the organization page
route that uses politicalOrganizationId (e.g.
revalidatePath(`/organization/${politicalOrganizationId}`) or your app’s org
path pattern); keep the existing return { ok: true, importedCount:
result.importedCount } and ensure you reference the same usecase.execute,
revalidatePath, politicalOrganizationId and PortfolioAsset symbols when adding
the extra calls.

In `@webapp/package.json`:
- Around line 34-35: The db:generate script in package.json uses a direct call
"prisma generate" while other places use "pnpm exec prisma generate", causing an
inconsistency; update the "db:generate" script (and optionally "build:vercel" if
chained) to call "pnpm exec prisma generate" instead of "prisma generate" so all
scripts consistently use pnpm exec (referencing the "db:generate" and
"build:vercel" script names to locate the change).

In `@webapp/src/app/api/refresh/route.ts`:
- Line 20: 組織データ更新時にキャッシュが正しく無効化されるよう、現在 revalidateTag("organizations", "max")
を呼んでいる箇所に対応する全てのローダー(load-transactions-page-data.ts、load-transactions-for-csv.ts、load-top-page-data.ts)でキャッシュ設定に
"organizations" タグを追加してください(例えば cache options の tags 配列に "organizations"
を含める)、もしくは PrismaPoliticalOrganizationRepository
を経由して組織データを取得していることを確認のうえ、その意図を明示してください。

In `@webapp/src/app/layout.tsx`:
- Line 42: Remove the global force-dynamic setting from the root layout (the
export const dynamic = "force-dynamic" in layout.tsx) so it doesn't force all
routes to dynamic rendering; instead, set dynamic = "force-dynamic" only on the
specific page components or route handlers that truly need dynamic behavior (or
remove it entirely) and keep per-page ISR/static optimizations like revalidate =
300 in the o/[slug]/page.tsx intact so that that page can use static
generation/ISR as intended.

In `@webapp/src/client/components/top-page/PortfolioSection.tsx`:
- Line 6: In PortfolioSection.tsx replace the relative import of
PortfolioPieChart (currently "./features/charts/PortfolioPieChart") with the
project's absolute-import form starting with "@/..." so the module import
follows the repo convention; update the import line that references
PortfolioPieChart accordingly and run typecheck/TS build to ensure the path
resolves.

In
`@webapp/src/server/contexts/public-finance/infrastructure/repositories/prisma-portfolio.repository.ts`:
- Line 60: The conversion Number(asset.amount) can lose precision for values
above Number.MAX_SAFE_INTEGER; instead preserve precision by storing/returning
the amount as a string (e.g., use asset.amount.toString() or keep the BigInt
string) or explicitly handle BigInt on the consumer side—update the code that
maps asset.amount (the Number(...) call) to return a string representation and
adjust any downstream consumers to parse BigInt/string as needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f39a6337-9038-48d6-9d93-35b22772de09

📥 Commits

Reviewing files that changed from the base of the PR and between bfeea3e and 0e7916e.

⛔ Files ignored due to path filters (5)
  • docs/templates/donor-template.csv is excluded by !**/*.csv and included by docs/**
  • docs/templates/mf-transactions-template.csv is excluded by !**/*.csv and included by docs/**
  • docs/templates/portfolio-template.csv is excluded by !**/*.csv and included by docs/**
  • webapp/public/logos/service-logo-pc.svg is excluded by !**/*.svg, !**/*.svg and included by webapp/**
  • webapp/public/logos/service-logo-sp.svg is excluded by !**/*.svg, !**/*.svg and included by webapp/**
📒 Files selected for processing (30)
  • admin/package.json
  • admin/src/app/(auth)/import-portfolio/page.tsx
  • admin/src/client/components/layout/Sidebar.tsx
  • admin/src/client/components/portfolio-csv-import/PortfolioCsvImportClient.tsx
  • admin/src/server/contexts/data-import/application/usecases/import-portfolio-csv-usecase.ts
  • admin/src/server/contexts/data-import/infrastructure/portfolio-csv/portfolio-csv-loader.ts
  • admin/src/server/contexts/data-import/presentation/actions/import-portfolio-csv.ts
  • admin/vercel.json
  • docs/upload-guide.md
  • prisma/migrations/20260222000000_add_portfolio_asset/migration.sql
  • prisma/schema.prisma
  • webapp/package.json
  • webapp/src/app/api/refresh/route.ts
  • webapp/src/app/api/test/route.ts
  • webapp/src/app/layout.tsx
  • webapp/src/app/o/[slug]/page.tsx
  • webapp/src/app/o/[slug]/transactions/page.tsx
  • webapp/src/app/sitemap.ts
  • webapp/src/client/components/common/ExplanationSection.tsx
  • webapp/src/client/components/layout/header/HeaderClient.tsx
  • webapp/src/client/components/top-page/PortfolioSection.tsx
  • webapp/src/client/components/top-page/features/charts/PortfolioPieChart.tsx
  • webapp/src/client/templates/maintenance-html.ts
  • webapp/src/server/contexts/public-finance/application/usecases/get-mock-transaction-page-data-usecase.ts
  • webapp/src/server/contexts/public-finance/application/usecases/get-portfolio-usecase.ts
  • webapp/src/server/contexts/public-finance/domain/models/portfolio.ts
  • webapp/src/server/contexts/public-finance/domain/repositories/portfolio-repository.interface.ts
  • webapp/src/server/contexts/public-finance/infrastructure/repositories/prisma-portfolio.repository.ts
  • webapp/src/server/contexts/public-finance/presentation/loaders/load-organizations.ts
  • webapp/src/server/contexts/public-finance/presentation/loaders/load-top-page-data.ts

Comment on lines +61 to +66
try {
const csvContent = await file.text();
const result = await importActionRef.current({
csvContent,
politicalOrganizationId,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does the browser File.text() API always decode file contents as UTF-8, making it unsuitable for preserving Shift_JIS CSV bytes before upload?

💡 Result:

Yes. File.text() (same as Blob.text()) always UTF‑8 decodes the underlying bytes into a JavaScript string, per the File API spec’s algorithm (“run UTF‑8 decode”) and MDN’s note that the data “is always presumed to be in UTF‑8 format.” [1] [2]

So if you call file.text() on a Shift_JIS-encoded CSV, you will not preserve the original bytes; you’ll get a UTF‑8–decoded string (often mojibake / replacement characters), making it unsuitable for a “byte-preserving before upload” path.

To preserve bytes for upload, send the File/Blob directly (e.g., FormData.append("file", file)) or read bytes via file.arrayBuffer() / file.bytes() instead of text(). If you actually want to decode Shift_JIS correctly in the browser, decode the ArrayBuffer with TextDecoder('shift_jis') (or use FileReader.readAsText(file, 'shift_jis'), which is explicitly different from text() in allowing other encodings). [1] [2]

Sources:
[1] File API (Blob text() method always UTF‑8 decodes)
[2] MDN: Blob.text() usage notes (always UTF‑8; contrasts with FileReader.readAsText())


File.text() では Shift_JIS 対応が壊れます

File.text() はブラウザ側で常に UTF-8 として文字列化するため(File API 仕様に従う)、Shift_JIS 形式の CSV ファイルを渡すと元のバイト列が失われます。結果として mojibake や置換文字が発生し、サーバー側でエンコーディング判定ができません。PR 要件の UTF-8 / Shift_JIS 両対応を実現するには、ここでは文字列ではなく arrayBuffer() で raw bytes を取得するか、FormData で直接ファイルを送信する必要があります。代替案として TextDecoder('shift_jis') でブラウザ側で Shift_JIS をデコードすることも可能です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@admin/src/client/components/portfolio-csv-import/PortfolioCsvImportClient.tsx`
around lines 61 - 66, File.text() を使うとブラウザ側で常に UTF-8 として文字列化され Shift_JIS
のバイト列が壊れるため、PortfolioCsvImportClient.tsx 内の importActionRef.current 呼び出しに渡す
csvContent を file.text() で作る実装をやめてください。代わりに file.arrayBuffer()
で生のバイト列を取得してサーバへ送るか、FormData に file をそのまま append して送信するように変更する——またはブラウザ側で対応するなら
TextDecoder('shift_jis') を使して適切にデコードしてから importActionRef.current に渡すように修正してください。

Comment on lines +118 to +123
<button
type="button"
onClick={handleImport}
disabled={!file || !politicalOrganizationId || isImporting}
className="px-6 py-2 rounded-full bg-primary text-primary-foreground text-sm font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:bg-primary/90"
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

管理画面の操作ボタンは shadcn の Button に寄せてください。

このセグメントだけ生の <button> と個別スタイルに戻っていて、admin UI の見た目と状態管理が分散します。

♻️ 修正案
-      <button
-        type="button"
-        onClick={handleImport}
-        disabled={!file || !politicalOrganizationId || isImporting}
-        className="px-6 py-2 rounded-full bg-primary text-primary-foreground text-sm font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:bg-primary/90"
-      >
+      <Button
+        type="button"
+        onClick={handleImport}
+        disabled={!file || !politicalOrganizationId || isImporting}
+      >
         {isImporting ? "インポート中..." : "インポート実行"}
-      </button>
+      </Button>

Button の import を @/client/components/ui に追加してください。

As per coding guidelines, "Use shadcn UI via index.ts exports".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@admin/src/client/components/portfolio-csv-import/PortfolioCsvImportClient.tsx`
around lines 118 - 123, Replace the raw <button> in PortfolioCsvImportClient
(the element using onClick={handleImport}, disabled={!file ||
!politicalOrganizationId || isImporting}, and the className string) with the
shadcn Button exported from "@/client/components/ui"; import { Button } from
"@/client/components/ui" at the top, then use <Button type="button"
onClick={handleImport} disabled={!file || !politicalOrganizationId ||
isImporting}> and map styling to Button props (e.g., variant/size) or keep
className if needed, ensuring the disabled and onClick behavior and isImporting
state handling remain unchanged.

Comment on lines +3 to +21
import type { PrismaClient } from "@prisma/client";
import type { PortfolioCsvLoader } from "@/server/contexts/data-import/infrastructure/portfolio-csv/portfolio-csv-loader";

export interface ImportPortfolioCsvInput {
csvContent: string;
politicalOrganizationId: string;
}

export interface ImportPortfolioCsvOutput {
importedCount: number;
}

const VALID_CATEGORIES = ["cash", "stocks", "precious_metals", "real_estate", "other"];

export class ImportPortfolioCsvUsecase {
constructor(
private readonly csvLoader: PortfolioCsvLoader,
private readonly prisma: PrismaClient,
) {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

ユースケースがインフラ実装に直接結合しています。

ImportPortfolioCsvUsecasePrismaClientPortfolioCsvLoader の具体実装に直接依存しており、application 層から infrastructure 層へ依存が逆流しています。ここはリポジトリ/ローダーの抽象に依存させて、実装の配線は presentation 側へ寄せたほうがこのコンテキストの境界を保てます。

As per coding guidelines, "docs/backend-architecture-guide.md: Bounded Context, layered architecture, dependency inversion."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@admin/src/server/contexts/data-import/application/usecases/import-portfolio-csv-usecase.ts`
around lines 3 - 21, ImportPortfolioCsvUsecase is directly depending on
infrastructure types (PrismaClient and PortfolioCsvLoader); change it to depend
on application-layer abstractions instead: define minimal interfaces in the
application layer such as PortfolioRepository (methods used to persist
portfolios) and CsvLoader (methods to parse CSV) and replace the constructor
signature of ImportPortfolioCsvUsecase to accept these interfaces rather than
PrismaClient or the infrastructure PortfolioCsvLoader; remove the PrismaClient
import and the infra import from this file, and update the composition
root/presentation layer to wire the concrete Prisma-based repository and
PortfolioCsvLoader implementation to those interfaces.

Comment on lines +43 to +46
if (!record.snapshotDate || !/^\d{4}-\d{2}-\d{2}$/.test(record.snapshotDate)) {
throw new Error(
`不正な日付形式です: "${record.snapshotDate}". YYYY-MM-DD形式で指定してください`,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

存在しない日付を通してしまいます。

この判定は書式しか見ていないので、2024-02-31 のような実在しない日付も通ります。上書き対象日と保存日が CSV の意図からずれるので、カレンダー上の実在日付かまで厳密に検証してから受け付けてください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@admin/src/server/contexts/data-import/application/usecases/import-portfolio-csv-usecase.ts`
around lines 43 - 46, 現在の validation は正規表現で YYYY-MM-DD 形式のみを確認しており `2024-02-31`
のような実在しない日を通してしまうため、record.snapshotDate の実在日チェックを追加してください:
import-portfolio-csv-usecase.ts の処理で record.snapshotDate を YYYY-MM-DD
で分割して年・月・日を数値化し、new Date(year, month-1, day) を生成してから date.getFullYear() /
date.getMonth()+1 / date.getDate() が元の値と一致するか(および生成した日付が有効か)を検証し、不一致や無効なら現在の
throw new Error(...) を用いて拒否するように修正してください。なお既存の /^\d{4}-\d{2}-\d{2}$/
チェックは残して構造チェック→実在日チェックの順で行ってください。

Comment on lines +55 to +74
// 同日付のデータを削除して上書き
const snapshotDates = [...new Set(records.map((r) => r.snapshotDate))];
for (const date of snapshotDates) {
await this.prisma.portfolioAsset.deleteMany({
where: {
politicalOrganizationId: orgId,
snapshotDate: new Date(date),
},
});
}

await this.prisma.portfolioAsset.createMany({
data: records.map((record) => ({
politicalOrganizationId: orgId,
snapshotDate: new Date(record.snapshotDate),
category: record.category,
label: record.label,
amount: BigInt(Math.round(Number(record.amount))),
})),
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

上書き処理が非原子的で、失敗時に既存データだけ消えます。

既存行の削除と新規行の作成が同じ DB トランザクションに入っていないため、削除後に insert が失敗すると、その snapshotDate のデータを失います。今回の import は 1 回の処理として原子的に扱うべきです。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@admin/src/server/contexts/data-import/application/usecases/import-portfolio-csv-usecase.ts`
around lines 55 - 74, 現在の削除(snapshotDates をループして
this.prisma.portfolioAsset.deleteMany
を呼ぶ)と挿入(this.prisma.portfolioAsset.createMany)が別トランザクションで実行されており途中で失敗すると既存データが失われるので、これらを単一の
DB トランザクションにまとめて原子的に実行するよう修正してください;具体的には snapshotDates の deleteMany 呼び出し群と
records を使った createMany を this.prisma.$transaction 内で実行する(もしくは tx
を受け取るトランザクションコールバック内で tx.portfolioAsset.deleteMany(...) をループしてから
tx.portfolioAsset.createMany(...) を呼ぶ)ことで、delete と create
を一括でロールバックできるようにしてください(参照箇所: snapshotDates, records,
this.prisma.portfolioAsset.deleteMany, this.prisma.portfolioAsset.createMany)。

Comment on lines +34 to +35
"build:vercel": "pnpm run db:generate && pnpm run build",
"db:generate": "cd .. && prisma generate",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

LGTM!

ビルドスクリプトの改善により、Vercelビルド時にマイグレーションが実行されなくなりました。これは適切なプラクティスです。

軽微な点として、admin側ではpnpm exec prisma generateを使用していますが、こちらはprisma generateを直接呼び出しています。動作に影響はありませんが、一貫性のために統一を検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/package.json` around lines 34 - 35, The db:generate script in
package.json uses a direct call "prisma generate" while other places use "pnpm
exec prisma generate", causing an inconsistency; update the "db:generate" script
(and optionally "build:vercel" if chained) to call "pnpm exec prisma generate"
instead of "prisma generate" so all scripts consistently use pnpm exec
(referencing the "db:generate" and "build:vercel" script names to locate the
change).

return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

revalidateTag("organizations", "max");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 組織リポジトリを使用しているがorganizationsタグがないローダーを検索

# PrismaPoliticalOrganizationRepository を使用しているファイルを検索
echo "=== PrismaPoliticalOrganizationRepository を使用しているファイル ==="
rg -l "PrismaPoliticalOrganizationRepository" --type ts

echo ""
echo "=== 上記ファイルのうち organizations タグを持つもの ==="
rg -l "PrismaPoliticalOrganizationRepository" --type ts | xargs rg -l '"organizations"' 2>/dev/null || echo "None found"

echo ""
echo "=== unstable_cache で organizations タグがないローダー ==="
rg -A 10 "unstable_cache" webapp/src/server/contexts/public-finance/presentation/loaders/ | rg -B 5 -A 5 "PrismaPoliticalOrganizationRepository"

Repository: team-mirai/marumie

Length of output: 11585


🏁 Script executed:

# Get the complete unstable_cache calls with their tags for each loader
echo "=== load-transactions-page-data.ts cache tags ==="
rg -A 20 "export const loadTransactionsPageData" webapp/src/server/contexts/public-finance/presentation/loaders/load-transactions-page-data.ts | head -30

echo ""
echo "=== load-transactions-for-csv.ts cache tags ==="
rg -A 20 "const loadTransactionsForCsv" webapp/src/server/contexts/public-finance/presentation/loaders/load-transactions-for-csv.ts | head -30

echo ""
echo "=== load-top-page-data.ts cache tags ==="
rg -A 20 "export const loadTopPageData" webapp/src/server/contexts/public-finance/presentation/loaders/load-top-page-data.ts | head -30

Repository: team-mirai/marumie

Length of output: 2530


🏁 Script executed:

# Get the complete unstable_cache call with tags for load-top-page-data.ts
echo "=== load-top-page-data.ts complete unstable_cache config ==="
rg -A 40 "export const loadTopPageData = unstable_cache" webapp/src/server/contexts/public-finance/presentation/loaders/load-top-page-data.ts | tail -20

echo ""
echo "=== load-transactions-page-data.ts complete function and tags ==="
cat -n webapp/src/server/contexts/public-finance/presentation/loaders/load-transactions-page-data.ts | grep -A 25 "loadTransactionsPageData"

Repository: team-mirai/marumie

Length of output: 1518


🏁 Script executed:

# Get the entire load-top-page-data.ts file around the unstable_cache call to see tags
wc -l webapp/src/server/contexts/public-finance/presentation/loaders/load-top-page-data.ts

# Find the line number of unstable_cache and read from there with enough lines
rg -n "export const loadTopPageData = unstable_cache" webapp/src/server/contexts/public-finance/presentation/loaders/load-top-page-data.ts

# Get the complete call - assuming it starts around line 12
sed -n '12,80p' webapp/src/server/contexts/public-finance/presentation/loaders/load-top-page-data.ts

Repository: team-mirai/marumie

Length of output: 3154


🏁 Script executed:

# Get the end of the file to see the unstable_cache closing with tags
tail -40 webapp/src/server/contexts/public-finance/presentation/loaders/load-top-page-data.ts

Repository: team-mirai/marumie

Length of output: 1385


組織データに依存する他のローダーのキャッシュが無効化されません。

revalidateTag("organizations") を追加していますが、以下のローダーも PrismaPoliticalOrganizationRepository を使用して組織データを取得しているにもかかわらず、"organizations" タグが設定されていません:

  • load-transactions-page-data.ts: タグなし(キャッシュオプションが { revalidate: CACHE_REVALIDATE_SECONDS } のみ)
  • load-transactions-for-csv.ts: tags: ["transactions-for-csv"] のみ
  • load-top-page-data.ts: キャッシュキーのみ(タグなし)

組織データが更新された際、これらのキャッシュは無効化されず、古いデータが表示される可能性があります。各ローダーに "organizations" タグを追加するか、この動作が意図したものであることを確認してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/app/api/refresh/route.ts` at line 20,
組織データ更新時にキャッシュが正しく無効化されるよう、現在 revalidateTag("organizations", "max")
を呼んでいる箇所に対応する全てのローダー(load-transactions-page-data.ts、load-transactions-for-csv.ts、load-top-page-data.ts)でキャッシュ設定に
"organizations" タグを追加してください(例えば cache options の tags 配列に "organizations"
を含める)、もしくは PrismaPoliticalOrganizationRepository
を経由して組織データを取得していることを確認のうえ、その意図を明示してください。

weight: ["400", "700"],
});

export const dynamic = "force-dynamic";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Next.js 16 App Router, if app/layout.tsx exports dynamic = "force-dynamic", does that force all descendant routes to render dynamically and override per-page revalidate / static optimization?

💡 Result:

Yes—putting export const dynamic = 'force-dynamic' in app/layout.tsx forces every route that includes that layout (i.e., essentially your whole App Router tree) to be dynamically rendered at request time, so descendant pages cannot be statically optimized/ISR’d via revalidate. In particular, dynamic = 'force-dynamic' (like revalidate = 0) skips the Full Route Cache and the Data Cache, meaning “components will be rendered and data fetched on every incoming request” and any per-page revalidate/static optimization won’t apply to those routes. [1][2]

This is why Vercel/Next.js guidance generally recommends keeping shared parent layouts on auto and only opting specific segments into dynamic behavior. [2][3]


root layout に force-dynamic を配置しないでください。

root layout で dynamic = "force-dynamic" を設定すると、配下のすべてのルートが動的レンダリングに強制され、webapp/src/app/o/[slug]/page.tsxrevalidate = 300 を含む per-page の静的最適化と ISR が機能しなくなります。ビルド時の DB アクセス回避が目的であれば、該当するページや route handler に限定して設定するべきです。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/app/layout.tsx` at line 42, Remove the global force-dynamic
setting from the root layout (the export const dynamic = "force-dynamic" in
layout.tsx) so it doesn't force all routes to dynamic rendering; instead, set
dynamic = "force-dynamic" only on the specific page components or route handlers
that truly need dynamic behavior (or remove it entirely) and keep per-page
ISR/static optimizations like revalidate = 300 in the o/[slug]/page.tsx intact
so that that page can use static generation/ISR as intended.

import CardHeader from "@/client/components/layout/CardHeader";
import MainColumnCard from "@/client/components/layout/MainColumnCard";
import type { PortfolioData } from "@/server/contexts/public-finance/domain/models/portfolio";
import PortfolioPieChart from "./features/charts/PortfolioPieChart";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

相対 import ではなく @/ の絶対 import に統一してください。

このファイルだけ ./features/... になっており、リポジトリの import 規約から外れています。

♻️ 修正案
-import PortfolioPieChart from "./features/charts/PortfolioPieChart";
+import PortfolioPieChart from "@/client/components/top-page/features/charts/PortfolioPieChart";
As per coding guidelines, "Use absolute paths starting with `@/` for TypeScript imports instead of relative paths".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import PortfolioPieChart from "./features/charts/PortfolioPieChart";
import PortfolioPieChart from "@/client/components/top-page/features/charts/PortfolioPieChart";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/client/components/top-page/PortfolioSection.tsx` at line 6, In
PortfolioSection.tsx replace the relative import of PortfolioPieChart (currently
"./features/charts/PortfolioPieChart") with the project's absolute-import form
starting with "@/..." so the module import follows the repo convention; update
the import line that references PortfolioPieChart accordingly and run
typecheck/TS build to ensure the path resolves.

(asset: Prisma.PortfolioAssetGetPayload<Record<string, never>>) => ({
category: asset.category,
label: asset.label,
amount: Number(asset.amount),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

BigIntからNumberへの変換における精度に注意

Number(asset.amount)はJavaScriptのNumber.MAX_SAFE_INTEGER(約9,007兆)を超える値で精度が失われる可能性があります。ポートフォリオの金額としては通常問題になりませんが、金融アプリケーションとして認識しておくべき点です。

将来的に非常に大きな金額を扱う可能性がある場合は、文字列として保持するか、フロントエンドでBigIntをサポートする方法を検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@webapp/src/server/contexts/public-finance/infrastructure/repositories/prisma-portfolio.repository.ts`
at line 60, The conversion Number(asset.amount) can lose precision for values
above Number.MAX_SAFE_INTEGER; instead preserve precision by storing/returning
the amount as a string (e.g., use asset.amount.toString() or keep the BigInt
string) or explicitly handle BigInt on the consumer side—update the code that
maps asset.amount (the Number(...) call) to return a string representation and
adjust any downstream consumers to parse BigInt/string as needed.

claude added 2 commits March 8, 2026 01:32
データ更新の運用を円滑にするため、以下を追加:
- MFクラウド取引CSV / 寄付者CSV / ポートフォリオCSV のテンプレートファイル
- 全アップロード機能の仕様・手順・トラブルシューティングをまとめた運用ガイド

https://claude.ai/code/session_01Wo1xtz4a6cPvFg43G5Y7fy
upload-guide.mdと同内容のdocxファイルを生成。
生成スクリプト (scripts/generate-upload-guide-docx.py) も同梱。

https://claude.ai/code/session_01Wo1xtz4a6cPvFg43G5Y7fy
@osahiroshima osahiroshima force-pushed the claude/upload-template-docs-Ku41N branch from 34a53c4 to f260621 Compare March 8, 2026 01:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/upload-guide.md`:
- Around line 266-268: The docs contradict themselves for the CSV field amount;
decide and document a single behavior: either require amount to be an integer
and treat decimals as invalid, or allow decimals and state exactly how they are
rounded. Update the text around amount and the surrounding CSV guidance
(references: amount, snapshotDate, and the CSV upload section) to explicitly
state the chosen rule (e.g., "amount must be a non-negative integer; rows with
non-integer amounts will be rejected" OR "amount may be a non-negative number
and will be rounded to nearest integer"), and adjust any validation/operation
wording (e.g., "小数点以下は四捨五入されます") to match that choice so the specification is
unambiguous for operators.
- Around line 227-269: Add a note in the "## 3. ポートフォリオ資産インポート(Portfolio CSV)"
section stating supported encodings and the Excel re-save warning: explicitly
list "Supported encodings: Shift_JIS and UTF-8" and instruct users not to
re-save the CSV in Excel to avoid character corruption; place this text near the
CSV仕様 or 注意事項 subsection so it's obvious for users importing portfolio CSVs
(reference headers: `category`, `label`, `amount`, `snapshotDate`).
- Around line 256-263: The upload steps under "アップロード手順" currently instruct
direct import via the admin page `/import-portfolio` and the final step "5.
「インポート」をクリック" without any preview or confirmation; update the doc to insert a
clear preview and confirmation step before the final import (e.g., after step 4
add "プレビューを表示して内容を確認し、同日付の全件置換である旨を明示する" and require an explicit confirmation or
modal) and note that the admin UI requires using the preview feature to review
changes before performing the replace-all operation to align guidance with the
actual `/import-portfolio` behavior and coding guidelines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 55117a96-c10d-46f9-9d6d-bae9f4886906

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7916e and f260621.

⛔ Files ignored due to path filters (5)
  • docs/templates/donor-template.csv is excluded by !**/*.csv and included by docs/**
  • docs/templates/mf-transactions-template.csv is excluded by !**/*.csv and included by docs/**
  • docs/templates/portfolio-template.csv is excluded by !**/*.csv and included by docs/**
  • docs/upload-guide.docx is excluded by !**/*.docx and included by docs/**
  • scripts/generate-upload-guide-docx.py is excluded by none and included by none
📒 Files selected for processing (1)
  • docs/upload-guide.md

Comment on lines +227 to +269
## 3. ポートフォリオ資産インポート(Portfolio CSV)

### 概要

期末時点での資産内訳(現金・株式・貴金属・不動産等)を登録するためのCSVインポート機能です。webapp公開サイトのポートフォリオ表示に反映されます。

- **管理画面パス**: `/import-portfolio`
- **テンプレート**: [`docs/templates/portfolio-template.csv`](templates/portfolio-template.csv)
- **最大行数**: 100行

### CSV仕様

| ヘッダー名 | 必須 | 説明 |
|---|---|---|
| `category` | Yes | 資産カテゴリ(下記参照) |
| `label` | Yes | 資産の表示名(例: 普通預金(みずほ銀行)) |
| `amount` | Yes | 金額(正の整数) |
| `snapshotDate` | Yes | 基準日(`YYYY-MM-DD` 形式) |

### 資産カテゴリ一覧

| category値 | 意味 |
|---|---|
| `cash` | 現金・預金 |
| `stocks` | 有価証券(株式・投資信託等) |
| `precious_metals` | 貴金属 |
| `real_estate` | 不動産 |
| `other` | その他の資産 |

### アップロード手順

1. テンプレートCSVをダウンロードし、資産情報を記入
2. admin管理画面 `/import-portfolio` を開く
3. 政治団体を選択
4. CSVファイルの内容をテキストエリアに貼り付けるか、ファイルを選択
5. 「インポート」をクリック

### 注意事項

- **同日付のデータは上書きされます**: 同じ `snapshotDate` のデータが既に存在する場合、その日付の全データが削除されてから新しいデータが挿入されます
- 定期的に更新する場合は、対象日付のデータを全行含めたCSVを用意してください(差分更新ではなく全件置換)
- `amount` は0以上の整数を指定してください(小数点以下は四捨五入されます)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ポートフォリオ節にも文字コードの注意書きを明記してください。

この節だけ Shift_JIS / UTF-8 対応と再保存時の文字化け注意が抜けており、実装仕様と運用ガイドがずれています。ポートフォリオCSVも同じ前提で扱うなら、この節内で明示した方が誤運用を防げます。As per coding guidelines, "Supported encodings: Shift_JIS and UTF-8; ensure encoding preserved (don’t re-save in Excel)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/upload-guide.md` around lines 227 - 269, Add a note in the "## 3.
ポートフォリオ資産インポート(Portfolio CSV)" section stating supported encodings and the Excel
re-save warning: explicitly list "Supported encodings: Shift_JIS and UTF-8" and
instruct users not to re-save the CSV in Excel to avoid character corruption;
place this text near the CSV仕様 or 注意事項 subsection so it's obvious for users
importing portfolio CSVs (reference headers: `category`, `label`, `amount`,
`snapshotDate`).

Comment on lines +256 to +263
### アップロード手順

1. テンプレートCSVをダウンロードし、資産情報を記入
2. admin管理画面 `/import-portfolio` を開く
3. 政治団体を選択
4. CSVファイルの内容をテキストエリアに貼り付けるか、ファイルを選択
5. 「インポート」をクリック

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ポートフォリオ手順にもプレビュー確認を入れてください。

この手順だと即時インポートに読めますが、同日付の全件置換を伴う操作なので、確認ステップなしの案内は危険です。実装がプレビュー前提なら誤案内ですし、前提でなくても他のCSV導線と運用が不統一になります。As per coding guidelines, "use admin UI to upload and preview before importing."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/upload-guide.md` around lines 256 - 263, The upload steps under
"アップロード手順" currently instruct direct import via the admin page
`/import-portfolio` and the final step "5. 「インポート」をクリック" without any preview or
confirmation; update the doc to insert a clear preview and confirmation step
before the final import (e.g., after step 4 add
"プレビューを表示して内容を確認し、同日付の全件置換である旨を明示する" and require an explicit confirmation or
modal) and note that the admin UI requires using the preview feature to review
changes before performing the replace-all operation to align guidance with the
actual `/import-portfolio` behavior and coding guidelines.

Comment on lines +266 to +268
- **同日付のデータは上書きされます**: 同じ `snapshotDate` のデータが既に存在する場合、その日付の全データが削除されてから新しいデータが挿入されます
- 定期的に更新する場合は、対象日付のデータを全行含めたCSVを用意してください(差分更新ではなく全件置換)
- `amount` は0以上の整数を指定してください(小数点以下は四捨五入されます)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

amount の仕様が自己矛盾しています。

「0以上の整数」と定義した直後に「小数点以下は四捨五入されます」と書くと、入力仕様がぶれます。BigInt/整数バリデーション前提なら、ここは「小数はエラー」に寄せないと運用者が誤ったCSVを作ります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/upload-guide.md` around lines 266 - 268, The docs contradict themselves
for the CSV field amount; decide and document a single behavior: either require
amount to be an integer and treat decimals as invalid, or allow decimals and
state exactly how they are rounded. Update the text around amount and the
surrounding CSV guidance (references: amount, snapshotDate, and the CSV upload
section) to explicitly state the chosen rule (e.g., "amount must be a
non-negative integer; rows with non-integer amounts will be rejected" OR "amount
may be a non-negative number and will be rounded to nearest integer"), and
adjust any validation/operation wording (e.g., "小数点以下は四捨五入されます") to match that
choice so the specification is unambiguous for operators.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants