File size: 8,923 Bytes
13240d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
using BakeryErp.Infrastructure.Database;

namespace BakeryErp.Infrastructure.Repositories;

public sealed class PettyCashRepository
{
    private readonly DatabaseOptions _options;

    public PettyCashRepository(DatabaseOptions options)
    {
        _options = options;
    }

    public IReadOnlyList<LocalPettyCashTransaction> List()
    {
        return LocalDataStoreFile.Load(_options)
            .PettyCashTransactions
            .OrderByDescending(static transaction => transaction.TransactionDate)
            .ThenBy(static transaction => transaction.TransactionNumber)
            .ToList();
    }

    public LocalPettyCashTransaction? Find(Guid id)
    {
        return LocalDataStoreFile.Load(_options)
            .PettyCashTransactions
            .FirstOrDefault(transaction => transaction.Id == id);
    }

    public PettyCashReport GetReport()
    {
        var transactions = LocalDataStoreFile.Load(_options).PettyCashTransactions;
        var approvedExpenses = transactions.Where(static transaction =>
            transaction.TransactionType == LocalPettyCashTransactionType.Expense &&
            transaction.Status is LocalPettyCashStatus.Approved or LocalPettyCashStatus.MonthlyClosed);
        var replenishments = transactions.Where(static transaction =>
            transaction.TransactionType == LocalPettyCashTransactionType.Replenishment &&
            transaction.Status is LocalPettyCashStatus.Approved or LocalPettyCashStatus.Replenished);

        return new PettyCashReport(
            transactions.Count,
            transactions.Count(static transaction => transaction.Status == LocalPettyCashStatus.PendingFinanceApproval),
            approvedExpenses.Sum(static transaction => transaction.Amount),
            transactions.Count(static transaction => transaction.Status == LocalPettyCashStatus.MonthlyClosed),
            replenishments.Sum(static transaction => transaction.Amount),
            replenishments.Sum(static transaction => transaction.Amount) - approvedExpenses.Sum(static transaction => transaction.Amount));
    }

    public IReadOnlyList<StorePettyCashSummary> GetStoreSummaries()
    {
        return LocalDataStoreFile.Load(_options)
            .PettyCashTransactions
            .Where(static transaction => transaction.Status != LocalPettyCashStatus.Cancelled)
            .GroupBy(static transaction => transaction.StoreName)
            .Select(group => new StorePettyCashSummary(
                group.Key,
                group.Count(),
                group.Where(static transaction => transaction.TransactionType == LocalPettyCashTransactionType.Expense).Sum(static transaction => transaction.Amount),
                group.Where(static transaction => transaction.TransactionType == LocalPettyCashTransactionType.Replenishment).Sum(static transaction => transaction.Amount)))
            .OrderByDescending(static summary => summary.ExpenseAmount)
            .ToList();
    }

    public SavePettyCashResult Save(LocalPettyCashTransaction transaction)
    {
        var store = LocalDataStoreFile.Load(_options);
        var transactionNumber = transaction.TransactionNumber.Trim().ToUpperInvariant();

        if (string.IsNullOrWhiteSpace(transactionNumber) ||
            string.IsNullOrWhiteSpace(transaction.StoreName) ||
            string.IsNullOrWhiteSpace(transaction.ExpenseCategory))
        {
            return SavePettyCashResult.ValidationError("零用金單號、門市與分類為必填。");
        }

        if (transaction.Amount <= 0)
        {
            return SavePettyCashResult.ValidationError("金額需大於 0。");
        }

        var duplicatedNumber = store.PettyCashTransactions.Any(existing =>
            existing.Id != transaction.Id &&
            string.Equals(existing.TransactionNumber, transactionNumber, StringComparison.OrdinalIgnoreCase));
        if (duplicatedNumber)
        {
            return SavePettyCashResult.ValidationError("零用金單號已存在,請改用其他單號。");
        }

        var existingTransaction = store.PettyCashTransactions.FirstOrDefault(existing => existing.Id == transaction.Id);
        if (existingTransaction is null)
        {
            transaction.Id = transaction.Id == Guid.Empty ? Guid.NewGuid() : transaction.Id;
            transaction.TransactionNumber = transactionNumber;
            transaction.StoreName = transaction.StoreName.Trim();
            transaction.ExpenseCategory = transaction.ExpenseCategory.Trim();
            transaction.Description = BlankToNull(transaction.Description);
            transaction.Status = LocalPettyCashStatus.PendingFinanceApproval;
            transaction.CreatedAt = DateTime.UtcNow;
            store.PettyCashTransactions.Add(transaction);
        }
        else
        {
            if (existingTransaction.Status is LocalPettyCashStatus.Approved or LocalPettyCashStatus.MonthlyClosed or LocalPettyCashStatus.Replenished)
            {
                return SavePettyCashResult.ValidationError("已審核、已月結或已撥補的零用金紀錄不可編輯。");
            }

            existingTransaction.TransactionNumber = transactionNumber;
            existingTransaction.StoreName = transaction.StoreName.Trim();
            existingTransaction.TransactionDate = transaction.TransactionDate;
            existingTransaction.TransactionType = transaction.TransactionType;
            existingTransaction.ExpenseCategory = transaction.ExpenseCategory.Trim();
            existingTransaction.Amount = transaction.Amount;
            existingTransaction.Description = BlankToNull(transaction.Description);
            existingTransaction.ReceiptImagePath = BlankToNull(transaction.ReceiptImagePath) ?? existingTransaction.ReceiptImagePath;
            existingTransaction.Status = LocalPettyCashStatus.PendingFinanceApproval;
            existingTransaction.UpdatedAt = DateTime.UtcNow;
        }

        LocalDataStoreFile.Save(_options, store);
        return SavePettyCashResult.Success();
    }

    public void Approve(Guid id)
    {
        UpdateStatus(id, LocalPettyCashStatus.Approved);
    }

    public void Reject(Guid id)
    {
        UpdateStatus(id, LocalPettyCashStatus.Rejected);
    }

    public void CloseMonthly(Guid id)
    {
        UpdateStatus(id, LocalPettyCashStatus.MonthlyClosed);
    }

    public void Replenish(Guid id)
    {
        UpdateStatus(id, LocalPettyCashStatus.Replenished);
    }

    public static string GetTransactionTypeName(LocalPettyCashTransactionType type)
    {
        return type switch
        {
            LocalPettyCashTransactionType.Expense => "門市支出",
            LocalPettyCashTransactionType.Replenishment => "零用金撥補",
            LocalPettyCashTransactionType.Adjustment => "調整",
            _ => type.ToString()
        };
    }

    public static string GetStatusName(LocalPettyCashStatus status)
    {
        return status switch
        {
            LocalPettyCashStatus.Draft => "草稿",
            LocalPettyCashStatus.PendingFinanceApproval => "待財務審核",
            LocalPettyCashStatus.Approved => "已審核",
            LocalPettyCashStatus.MonthlyClosed => "已月結",
            LocalPettyCashStatus.Replenished => "已撥補",
            LocalPettyCashStatus.Rejected => "已退回",
            LocalPettyCashStatus.Cancelled => "已取消",
            _ => status.ToString()
        };
    }

    private void UpdateStatus(Guid id, LocalPettyCashStatus status)
    {
        var store = LocalDataStoreFile.Load(_options);
        var transaction = store.PettyCashTransactions.FirstOrDefault(existing => existing.Id == id);
        if (transaction is null)
        {
            return;
        }

        transaction.Status = status;
        transaction.UpdatedAt = DateTime.UtcNow;
        if (status == LocalPettyCashStatus.Approved)
        {
            transaction.ApprovedAt = DateTime.UtcNow;
        }
        if (status == LocalPettyCashStatus.MonthlyClosed)
        {
            transaction.ClosedAt = DateTime.UtcNow;
        }

        LocalDataStoreFile.Save(_options, store);
    }

    private static string? BlankToNull(string? value)
    {
        return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
    }
}

public sealed record PettyCashReport(
    int TransactionCount,
    int PendingFinanceApprovalCount,
    decimal ApprovedExpenseAmount,
    int MonthlyClosedCount,
    decimal ReplenishmentAmount,
    decimal EstimatedBalance);

public sealed record StorePettyCashSummary(
    string StoreName,
    int TransactionCount,
    decimal ExpenseAmount,
    decimal ReplenishmentAmount);

public sealed record SavePettyCashResult(bool IsSuccess, string? ErrorMessage)
{
    public static SavePettyCashResult Success() => new(true, null);
    public static SavePettyCashResult ValidationError(string message) => new(false, message);
}