しんたろーのITアカデミー
開発日記

UIを少し直すつもりが、気づけば複雑な税務システムを組まされていた。1人開発の泥沼。

UIを少し直すつもりが、気づけば複雑な税務システムを組まされていた。1人開発の泥沼。
しんたろーしんたろー
16分で読めます
この記事の内容(目次)
※この記事は、Claude Codeで1人開発しているSNS運用SaaS「ThreadPost」の開発日記です。

SNS運用を自動化しませんか?

ThreadPostなら、投稿作成・画像生成・スケジュール管理までAIがサポート。

無料で始める

画面のチカチカを直すだけの夜だった

クレジット残高の数字が、画面を開くたびに点滅していた。

ただそれだけのことだった。

UI修正から税務コンプライアンスの深淵へ
UI修正から税務コンプライアンスの深淵へ

コードは合っている。

APIも叩けている。

でもUIが揺れる。

この小さな不快感を消すためにキーボードを叩き始めた。

それが、法務と税務の底なし沼への入り口だった。

今日のコミットは50件。新機能が2件で、バグ修正が2件

残りの46件は、アフィリエイト報酬の税金計算、マイナンバーの暗号化保存、支払調書の自動生成、そしてWebhookの冪等性確保だ。

画面のチラつきを直すつもりが、企業なら法務部と経理部が数ヶ月かける要件を1人で書く羽目になった。

今日の開発スタッツ:50コミットの激闘
今日の開発スタッツ:50コミットの激闘

Webhookが3回飛んできて、報酬が3倍になっていた

始まりは「Fix flickering indicator: optimize credit fetching and implement smart polling」というコミットだった。

クレジット残高を取得するポーリングが毎回走り、再レンダリングで画面が揺れていた。

APIコールを束ねて、遅延読み込みを入れた。

チカチカは消えた。5分で終わった。

ただし、その後が長かった。

アフィリエイト機能のテスト中に別の異変に気づいた。

報酬の計算がおかしい。

「アフィリエイトシステムの重大なバグ修正と税務対応準備」というコミットを打つ羽目になった。

Webhookが複数回飛んできたとき、同じ報酬が二重、三重に付与されていた。

Stripeなどの決済基盤では、Webhookの到達保証のために同じイベントが複数回送信されることがある。

これは分散システムにおけるAt-Least-Once(少なくとも1回)と呼ばれる業界標準の仕様だ。

ネットワークの瞬断やタイムアウトを考慮し、確実な伝達を優先している。

だから受け手側で冪等性を担保しないと、システムは簡単に崩壊する。

今回の場合、Lightプランの報酬596円が3回付与され、1回のテストで1,192円の過払いが発生していた。

テスト環境だから笑えるが、本番なら致命傷だ。

しんたろーしんたろー:
1件1,200円の赤字。100件来たら12万円。
SaaSの利益なんて一瞬で吹き飛ぶ。笑えない。

すぐにデータベースにユニーク制約を追加した。

トランザクションIDとパートナーIDの組み合わせで弾くようにした。

「Cleanup duplicate bonus triggers and add idempotency check」で重複トリガーを掃除した。

単に処理済みフラグを立てるだけでは、ミリ秒単位で並行してリクエストが来た時の競合状態を防げない。

データベースのトランザクション分離レベルを意識し、UPSERT処理で確実にはじく必要がある。

「add onConflict parameter to user_credits upsert in webhook」というコミットで、データベースレベルでの排他制御を入れた。

アフィリエイト報酬を支払うということは、法律上、税務処理が発生する。

年間5万円を超えたら支払調書が必要になる。

マイナンバーも収集しなければならない。

僕は「Phase 1 - 源泉徴収税計算と税務UI機能」の実装に入った。

報酬額から源泉徴収税を計算し、手取り額を出す。

さらにマイナンバーを安全に保存するため、「save_mynumber_to_vault」というデータベース関数を書いた。

SupabaseのVault機能を使って、暗号化して保存する仕組みだ。

平文で保存すれば、データベースがハッキングされた瞬間に全パートナーの個人情報が流出する。

ここでPostgreSQLのインデックス制約という壁にぶつかった。

「マイグレーションのインデックス作成エラーを修正」というコミットだ。

日付から月や年を抽出するEXTRACT関数を使ってインデックスを作ろうとした。

しかし、PostgreSQLは関数の結果が不変、つまりIMMUTABLEでないとインデックス作成を拒否する。

タイムゾーンやサーバーの設定によって結果が変わる関数は、インデックスのキーとして使えない。

これはデータベースの整合性を守るための厳格な仕様だ。

しんたろーしんたろー:
インデックス1個張るのにPostgresの内部仕様と格闘。
Claude、平気で動かないSQLを吐いてくるんだよな。3回連続で同じエラー出た。

「EXTRACT関数の使用を日付範囲比較に変更」でクエリ自体を書き換えた。

インデックス側ではなく、検索条件側で期間を指定するようにした。

さらに、パートナーのダッシュボードで権限エラーが頻発した。

「パートナーダッシュボードのRLSポリシー問題と印刷機能を修正」というコミット。

Supabaseの行レベルセキュリティ、つまりRLSが、関連テーブルの権限不足でエラーを吐いていた。

PostgreSQLはクエリ実行時にすべてのポリシーを評価する。

一つのテーブルへのアクセス権があっても、JOIN先のテーブルの権限がないと全体が弾かれる。

「public.users」テーブルへのアクセス権限がないために、「partners」テーブルのデータまで取得できなくなっていた。

APIの呼び出し頻度についても見直しを迫られた。

「アカウント連携制限とAPI分析収集を最適化」というコミットだ。

外部APIのレート制限に引っかかり、データの取得が頻繁に失敗していた。

APIの呼び出し間隔を1秒から5秒に延ばし、収集期間を30日から7日に短縮した。

これにより、APIの呼び出し数を75%削減できた。

複数のリクエストを1つのRPC関数にまとめる処理も入れた。

認証チェックの回数を減らすことで、ダイアログの表示速度が13秒から1秒以下になった。

「Defer plan info loading for instant dialog opening」で、重い処理をバックグラウンドに回した。

画面のチカチカを直すだけのつもりが、決済インフラの非同期処理とデータベースの権限モデルの深淵に引きずり込まれた。

ここまでで既に数時間が経っていた。

PDFライブラリを捨てて、ブラウザに丸投げした

泥沼のバグ修正を終え、「アフィリエイト税務システム設計書 v2.2 完成」というコミットをPushした。

ここからは攻めの実装だ。

税務対応として以下の機能を一気に実装した。

  • 源泉徴収税の自動計算
  • マイナンバーの暗号化保存
  • 支払調書の自動生成
  • 税務監査ログの記録
  • 5万円超過時の自動通知

パートナー向けの支払調書を自動生成する機能に着手した。

「支払調書自動生成機能実装」というコミットで、年度ごとにデータを一括生成する仕組みを作った。

当初はサーバーサイドでPDFを生成するライブラリを導入した。

「支払調書PDF生成・ダウンロード機能を実装」というコミットだ。

React-PDFなどのライブラリは、サーバー側でドキュメントを組み立てる。

しかし、PDF生成において以下の問題が頻発した。

  • 日本語フォントの文字化け
  • 改行位置の不自然なズレ
  • A4サイズへのレイアウト崩れ
  • サーバーのメモリ消費増大

PDFライブラリの仕様に合わせるために、何時間もCSSをこねくり回した。

サーバーサイドでのPDF生成は、パフォーマンスのボトルネックになりやすい。

そこで「HTMLプレビュー + ブラウザ印刷で支払調書をPDF化」というコミットで、アプローチを根本から変えた。

サーバーでPDFを作るのをやめた。

ブラウザの印刷機能を使えば、ChromeやSafariの強力なレンダリングエンジンをそのまま利用できる。

ライブラリのバグに付き合う時間より、ブラウザの標準機能に乗っかる方が早い。

「ボトムメニュー非表示&1ページ印刷を改善」で余計なUI要素を消し、余白をミリ単位で調整した。

印刷ボタンを押せば、完璧なレイアウトの支払調書がプレビューされる。

ブラウザの印刷機能であるPrint CSSは、実は複雑な帳票出力において最も堅牢な回避策となる。

ページ分割や余白の設定も、CSSの@pageルールを使えば簡単に制御できる。

「支払調書印刷を1ページに収まるように調整」というコミットで、A4サイズにピタリと収まるようにした。

これで、パートナーは迷わず自分の支払調書を出力できる。

さらに、税務監査ログシステムも組み込んだ。

「Phase 3.2 - 税務監査ログシステムの実装」だ。

誰が、いつ、どの税務データを操作したか。

すべてデータベースに記録される。

「tax_audit_logs」テーブルを作り、管理者は全ログを閲覧可能にした。

税務調査が入っても、堂々とデータを提示できる。

マイナンバーの収集についても、大きな方針転換をした。

「本人確認方針を明確化(Stripe Connect委託)」というコミット。

自前でマイナンバーを収集・保管するリスクは大きすぎる。

万が一漏洩すれば、サービスは即終了だ。

本人確認、いわゆるKYCは法規制が厳しく、スタートアップが負うべきリスクではない。

だから、本人確認のプロセスをすべてStripe Connectにオフロードした。

Stripe Connectにオフロードした理由は以下の通りだ。

  • 本人確認の法的リスク回避
  • セキュリティ要件の緩和
  • 開発コストの大幅削減

自前で作った暗号化保存の仕組みは無駄になった。

ただ、それ以上の価値がある判断だった。

アカウント削除時の処理にも手を入れた。

「ユーザー削除時にreferralsテーブルも削除」と「ユーザー削除時にpartnersテーブルも削除」というコミットだ。

ユーザーが退会した時、関連するアフィリエイトデータが残っていると、外部キー制約エラーが発生する。

親レコードを削除する前に子レコードを綺麗に掃除しなければならない。

紹介コードの処理も改善した。

「紹介コードのCookie処理を修正」というコミット。

サブドメイン間での共有を可能にし、リダイレクト時も紹介コードを引き継ぐようにした。

アフィリエイトシステムにおいて、トラッキングの漏れはパートナーの不信感に直結する。

しんたろーしんたろー:
書いたコードを自分で捨てた。
Phase 2で暗号化保存まで作り込んで、「やっぱStripeに任せます」。
しんどいけど、これが正解だと思う。たぶん。

ここまで読んだあなたに

今なら無料で全機能をお試しいただけます。設定後はAIが投稿案を毎日生成。確認して選ぶだけ。

無料で始める

落とし穴:Claude、お前が吐いたSQLが動かなかった件

EXTRACT関数のインデックスエラーは、Claudeが生成したSQLをそのまま実行したら即死したやつだ。

3回連続で同じパターンのSQLを出してきた。

「EXTRACT(MONTH FROM created_at)」でインデックスを張ろうとする。

PostgreSQLが「IMMUTABLEじゃないから無理」と蹴る。

Claudeが「じゃあこうしましょう」と言って、また同じEXTRACTを使う。

まじかよ、と思いながら自分でクエリを書き直した。

インデックス側で関数を使うのをやめて、WHERE句で日付範囲を直接指定する形に変えた。

次からはPostgreSQL側の制約を先に確認してからClaudeに投げる。

たぶん。

今日の数字

| 指標 | 今日の数字 | 比較対象 |

|------|-----------|----------|

| コミット数 | 50件 | 普段の平均は15件 |

| 新機能追加 | 2件 | 普段の平均は1件 |

| Webhook過払い防止 | 1,192円/回 | 月100件なら12万円の損失 |

| ダイアログ表示速度 | 13秒→1秒以下 | 改善率92% |

| API呼び出し削減 | 75%削減 | 30日→7日収集期間に短縮 |

| 支払調書生成 | 1秒 | 手作業なら1件15分 |

企業なら法務部と経理部が数ヶ月かけて要件定義する税務コンプライアンス基盤を、1人が1日で組んだ。

Webhookの冪等性、RLSポリシー、税務監査ログ、支払調書の自動生成。

コミット50件の内訳がそれを物語っている。

FAQ

Stripe Connectへの移行で、実際に削減できた開発コストはどのくらいか?

マイナンバー暗号化保存のデータベース関数まで実装し終えていた。

その後、法改正時の対応コストとセキュリティ監査の負担を計算したら、自前管理は割に合わなかった。

書いたコードを捨てることになっても、KYCは外部委託する方が開発工数で数十時間、リスクコストで数百万円単位の節約になる。

PostgreSQLでEXTRACT関数を使ったインデックスが作れない時の直し方は?

EXTRACTはタイムゾーン依存でIMMUTABLEではないため、PostgreSQLがインデックスキーとして拒否する。

インデックス側で関数を使うのをやめて、WHERE句で「created_at >= '2024-01-01' AND created_at < '2024-02-01'」のように日付範囲を直接指定する形に書き換えると解決する。

Claudeに投げると3回連続で同じEXTRACTを使ったSQLを出してくるので、この制約は自分で把握しておく方が早い。

Webhookの冪等性を担保しないと、実際にどんな金額的被害が出るのか?

今回のテストでは596円の報酬が3回付与されて1,192円の過払いが発生した。

本番環境で月100件のWebhookが来れば、それだけで月12万円の損失になる計算だ。

DBのユニーク制約でトランザクションIDとパートナーIDの組み合わせを弾くのが、最もシンプルで確実な対策だった。

まとめ

UIを直すつもりが税務基盤を作っていた。

Webhookの重複報酬、PostgreSQLのインデックス制約、RLSポリシーの権限エラー。

全部今日のコミット50件に刻まれている。

👉 ThreadPostでSNS運用を自動化する

ThreadPost — SNS投稿をAIが自動化

この記事が参考になったら、ThreadPostを試してみませんか?投稿作成・画像生成・スケジュール管理まで、AIがサポートします。

無料で始める

この記事をシェア

XはてブLINE
しんたろー

ThreadPost開発者・個人開発エンジニア

AI × SaaS個人開発者。Cursor / Claude Code を使った効率的開発、SNS自動化について実体験から発信。

人気の記事