【ポートフォリオ】Laravel + AWS + jQueryで構築したサブスク管理アプリ「SubsCut」の解説


# サブスク管理アプリ　　SubsCut  ~サブスカット~
![portfolio-movie-ezgif.com-video-to-gif-converter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/065f2f87-91f3-4a2a-852c-59ec171adc49.gif)
URL　https://subscut.site
GitHub　https://github.com/cafelatte00/subscut_aws
自作したアプリの**設計〜実装までの流れ**をご紹介いたします！

# 使用技術

| 項目 | 内容 |
|------|------|
| バックエンド | PHP 8 / Laravel 9 |
| フロントエンド | JavaScript / jQuery / Chart.js |
| メールサーバー | Gmail SMTP |
| インフラ | AWS（EC2 / RDS[MySQL] / S3 / Route 53 / ACM / IAM） |

# 作成背景
お試し感覚で入ったサブスクの課金日が過ぎてしまい、もったいないと思ったのが、作成のきっかけです。

# アプリ概要
- サブスクを登録し、いつでも一覧から次回課金日・料金等を確認できる
- 課金日の１週間前からメールで通知
- 過去２年分の課金額をグラフで確認できる

# 機能一覧
| | 機能 |  |
|--------|--------|--------|
| 1 | ログイン機能 |  |
| 2 | サブスクの登録（CRUD） | 登録は非同期通信 |
| 3 | サブスクの解約|  |
| 4 | グラフ表示機能 | Chart.js |
| 5 | ページネーション機能 |  |
| 6 | ゲストユーザーログイン機能 |  |
| 7 | バッチでサブスク更新・メールで課金日通知 |  |
| 8 | プロフィール画像の保存・削除 |  |

# 画面遷移図
※企画段階と実装段階で、変更した箇所があります。
![スクリーンショット 2025-05-18 20.16.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/c8a90d15-2747-418e-abe3-9b02664dca3b.png)

# モックアップ
※企画段階と実装段階で、デザイン変更しています。
![スクリーンショット 2025-05-18 20.35.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/07ffd0fb-9382-437a-8545-36fe2e5d87f4.png)

# ER図
![スクリーンショット 2025-05-18 21.18.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/dfb05110-44c2-4b1e-aa6d-9e3417fc9b20.png)
ツール：https://dbdiagram.io

# インフラ構成図
![AWS.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/5d26f50b-43c4-4ec2-a92d-d30e8c1123c9.png)

# 使用イメージ
#### サブスクの登録
サブスク登録は非同期通信を使用。
![2025-05-1712.52.28-ezgif.com-speed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/a6c0cceb-5ca2-452f-b5ce-e7c6560f980b.gif)

サブスクの一覧表示・詳細・編集・削除は同期通信。
![2025-05-1716.35.47-ezgif.com-speed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/f40e7972-0eb3-426f-827c-b8e35dadb9ac.gif)

#### サブスクの解約
実際のサブスクを解約したら、Subscutアプリでも「解約ボタン」を押すと、ステータスが「解約済」に変わります。解約日の翌日以降は課金されていないという仕様で、棒グラフが表示されます。
![2025-05-1714.04.15-ezgif.com-speed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/6c2c9bb5-788d-4eac-b486-3441d8542b72.gif)

#### グラフ表示
過去２年間の課金額が月別で表示されます。
![2025-05-1713.30.24-ezgif.com-speed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/6ee8ccde-573b-4391-81db-d6875e2e46ff.gif)

#### メールで課金日をお知らせ
毎日0:00にバッチでサブスクをチェック＆更新。課金日の１週間前から16:00にメールでお知らせ。
![スクリーンショット 2025-05-18 16.22.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/5ddb5891-500a-4e8c-bfee-19881c245ac5.png)

#### プロフィール画像の保存・削除
![画面収録_2025_05_17_16_57_17_V1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4089961/08808425-d02d-4ab2-9546-e4252cc5cc34.gif)

# テスト  
手動テストを中心で、一部PHPUnitを取り入れました。


**・手動テスト**
本番環境での動作確認は、スプレッドシートで管理し検証しました。  
https://docs.google.com/spreadsheets/d/1faCWt7w9OxA7q8PvEJ3xfrJYhpJlIxT1TvgiqOAYA1k/edit?usp=sharing

**・PHPUnit**
**Featureテスト**　サブスクリプション登録機能テスト
```
/**
 * 概要: addSubscriptionメソッドが新しいサブスクを作成し、JSONレスポンスを返すことを確認する
 * 条件: 有効なサブスクデータをPOSTリクエストとして送信する
 * 結果: ステータスコード200が返され、データベースに新しいサブスクが存在する
 */
public function test_addSubscriptionメソッドが新しいサブスクを新規作成し、JSONレスポンスを返すことを確認する()
{
    $data = [
        'user_id' => $this->user->id,
        'title' => 'Test Subscription',
        'price' => 1,
        'frequency' => 1,
        'first_payment_day' => '2025-03-01',
        'url' => 'http://test.com',
        'memo' => 'Test memo',
    ];

    $response = $this->postJson(route('subscriptions.add.subscription'), $data);

    $response->assertStatus(200);
    $this->assertDatabaseHas('subscriptions', ['title' => 'Test Subscription']);
}
```
　
**Unitテスト** 支払い関連のサービス(次回支払日と支払い回数の計算)のテスト
```
    /**
     * 概要: CheckSubscriptionService::calculatePaymentDetails() メソッドが、正しい次回支払日と支払い回数の値を返すことを確認す
     * 条件: 初回支払日が過去、支払い頻度が 1(１ヶ月) の引数を用意し、calculatePaymentDetails() メソッドを実行する。
     * 結果: メソッドの戻り値が 次回支払日は2025-04-01と同じ、支払い回数は3であること。
     * 注意：次回支払日の日付はテストするに日によって書き換えること。
     */
    public function test_初回支払日が過去の場合、正しい次回支払日と支払い回数を返す()
    {
        $firstPaymentDay = Carbon::parse('2025-01-01');
        $frequency = 1;

        $paymentDetails = CheckSubscriptionService::calculatePaymentDetails($firstPaymentDay, $frequency);

        $this->assertEquals(Carbon::parse('2025-04-01'), $paymentDetails['nextPaymentDay']);

        $this->assertEquals(3, $paymentDetails['numberOfPayments']);
    }

```

# 気をつけた点
月によって月末日が違うため、次回支払日がずれないようテストを何回も行いながら気をつけて実装しました。

・実装の一部を掲載します
**app/Services/CheckSubscriptionService.php**
```php
    /**
     * 初回支払日と頻度から次回支払日と支払い回数を計算します。
     *
     * @param Carbon $firstPaymentDay 初回支払日
     * @param int $frequency 支払い頻度（月単位）
     * @return array 次回支払日と支払い回数を含む連想配列
     */
    public static function calculatePaymentDetails(Carbon $firstPaymentDay, int $frequency): array
{
    $today = Carbon::today();
    $nextPaymentDay = null;
    $numberOfPayments = 0;

    if ($today->eq($firstPaymentDay)) {
        // 本日が初回支払日の場合
        $nextPaymentDay = $firstPaymentDay->copy()->addMonthNoOverflow($frequency);
        $numberOfPayments = 1;
    } elseif ($today->lt($firstPaymentDay)) {
        // 初回支払日が未来の場合
        $nextPaymentDay = $firstPaymentDay;
    } else {
        // 初回支払日が過去の場合
        $numberOfPayments = 1;
        $calcPaymentDay = $firstPaymentDay->copy()->addMonthNoOverflow($frequency);
        $nextPaymentDay = $calcPaymentDay;

        // 計算用の支払日より今日が大きい間、繰り返す
        while ($today->gte($calcPaymentDay)) {
            $numberOfPayments++;
            $calcPaymentDay = $calcPaymentDay->copy()->addMonthNoOverflow($frequency);
            $nextPaymentDay = $calcPaymentDay;
        }
    }

    return [
        'nextPaymentDay' => $nextPaymentDay,
        'numberOfPayments' => $numberOfPayments,
    ];
}

```
https://github.com/cafelatte00/subscut_aws/blob/master/app/Services/CheckSubscriptionService.php

# 感想
ポートフォリを作ってみて、企画したことが形になっていくのが楽しかったです！またMVCモデルなのでRuby on Rails とLaravelは似ていると感じました。
最後まで読んでいただき、ありがとうございました！