package main
import (
"context"
"fmt"
"mawinter-server/internal/factory"
"time"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
type fixMonthlyOption struct {
Logger *zap.Logger
DBInfo struct {
Host string
Port string
User string
Pass string
Name string
}
}
var fixMonthlyOpt fixMonthlyOption
// fixMonthlyOptCmd represents the start command
var fixMonthlyOptCmd = &cobra.Command{
Use: "fixmonth",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return startFixMonthly()
},
}
func startFixMonthly() (err error) {
l, err := factory.NewLogger()
if err != nil {
fmt.Println(err)
return err
}
defer l.Sync()
db, err := factory.NewDBRepositoryV1(fixMonthlyOpt.DBInfo.Host, fixMonthlyOpt.DBInfo.Port, fixMonthlyOpt.DBInfo.User, fixMonthlyOpt.DBInfo.Pass, fixMonthlyOpt.DBInfo.Name)
if err != nil {
l.Error("failed to connect DB", zap.Error(err))
return err
}
defer db.CloseDB()
mc := factory.NewMailClient()
ap := factory.NewRegisterService(l, db, mc)
ctx := context.Background()
return ap.InsertMonthlyFixBilling(ctx, time.Now().Local().Format("200601"))
}
func init() {
rootCmd.AddCommand(fixMonthlyOptCmd)
fixMonthlyOptCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
fixMonthlyOptCmd.Flags().StringVar(&fixMonthlyOpt.DBInfo.Host, "db-host", "mawinter-db", "DB Host")
fixMonthlyOptCmd.Flags().StringVar(&fixMonthlyOpt.DBInfo.Port, "db-port", "3306", "DB Port")
fixMonthlyOptCmd.Flags().StringVar(&fixMonthlyOpt.DBInfo.Name, "db-name", "mawinter", "DB Name")
fixMonthlyOptCmd.Flags().StringVar(&fixMonthlyOpt.DBInfo.User, "db-user", "root", "DB User")
fixMonthlyOptCmd.Flags().StringVar(&fixMonthlyOpt.DBInfo.Pass, "db-pass", "password", "DB Pass")
}
package main
func main() {
Execute()
}
package main
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "root",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bill-manager.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package main
import (
"context"
"fmt"
"mawinter-server/internal/factory"
"mawinter-server/internal/server"
"mawinter-server/internal/timeutil"
"time"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var jst *time.Location
func init() {
j, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
jst = j
}
type DuplicateCheckOption struct {
Logger *zap.Logger
DBInfo struct {
Host string
Port string
User string
Pass string
Name string
}
Lastmonth bool // if true, process last month table
}
var duplicateCheckOpt DuplicateCheckOption
// duplicateCheckCmd represents the duplicateCheck command
var duplicateCheckCmd = &cobra.Command{
Use: "duplicateCheck",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("duplicateCheck called")
Run()
},
}
func init() {
rootCmd.AddCommand(duplicateCheckCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// duplicateCheckCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
duplicateCheckCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
duplicateCheckCmd.Flags().StringVar(&duplicateCheckOpt.DBInfo.Host, "db-host", "mawinter-db", "DB Host")
duplicateCheckCmd.Flags().StringVar(&duplicateCheckOpt.DBInfo.Port, "db-port", "3306", "DB Port")
duplicateCheckCmd.Flags().StringVar(&duplicateCheckOpt.DBInfo.Name, "db-name", "mawinter", "DB Name")
duplicateCheckCmd.Flags().StringVar(&duplicateCheckOpt.DBInfo.User, "db-user", "root", "DB User")
duplicateCheckCmd.Flags().StringVar(&duplicateCheckOpt.DBInfo.Pass, "db-pass", "password", "DB Pass")
duplicateCheckCmd.Flags().BoolVar(&duplicateCheckOpt.Lastmonth, "last-month", false, "if true, process last month table")
}
func Run() (err error) {
var YYYYMM string // roc month table name (YYYYMM
thisMonth := time.Date(timeutil.NowFunc().Year(), timeutil.NowFunc().Month(), 1, 0, 0, 0, 0, jst)
if !duplicateCheckOpt.Lastmonth {
YYYYMM = timeutil.NowFunc().Format("200601")
} else {
// last month
YYYYMM = thisMonth.AddDate(0, -1, 0).Format("200601")
}
l, err := factory.NewLogger()
if err != nil {
fmt.Println(err)
return err
}
defer l.Sync()
l.Info("binary info", zap.String("version", version), zap.String("revision", revision), zap.String("build", build))
server.Version = version
server.Revision = revision
server.Build = build
db, err := factory.NewDBRepositoryV2(startOpt.DBInfo.Host, startOpt.DBInfo.Port, startOpt.DBInfo.User, startOpt.DBInfo.Pass, startOpt.DBInfo.Name)
if err != nil {
l.Error("failed to connect DB", zap.Error(err))
return err
}
defer db.CloseDB()
ctx := context.Background()
ap := factory.NewServiceV2(l, db)
svc := factory.NewDuplicateCheckService(l, ap)
l.Info("proc month table name", zap.String("YYYYMM", YYYYMM))
return svc.DuplicateCheck(ctx, YYYYMM)
}
package main
func main() {
Execute()
}
package main
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "server",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bill-manager.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package main
import (
"context"
"fmt"
"mawinter-server/internal/factory"
"mawinter-server/internal/server"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
type StartOption struct {
Logger *zap.Logger
DBInfo struct {
Host string
Port string
User string
Pass string
Name string
}
BasicAuth struct {
User string
Pass string
}
}
var (
version string
revision string
build string
)
var startOpt StartOption
// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return start()
},
}
func start() (err error) {
l, err := factory.NewLogger()
if err != nil {
fmt.Println(err)
return err
}
defer l.Sync()
db2, err := factory.NewDBRepositoryV2(startOpt.DBInfo.Host, startOpt.DBInfo.Port, startOpt.DBInfo.User, startOpt.DBInfo.Pass, startOpt.DBInfo.Name)
if err != nil {
l.Error("failed to connect DB", zap.Error(err))
return err
}
defer db2.CloseDB()
ap2 := factory.NewServiceV2(l, db2)
srv := factory.NewServer(l, ap2)
ctx := context.Background()
l.Info("binary info", zap.String("version", version), zap.String("revision", revision), zap.String("build", build))
server.Version = version
server.Revision = revision
server.Build = build
return srv.Start(ctx)
}
func init() {
rootCmd.AddCommand(startCmd)
startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
startCmd.Flags().StringVar(&startOpt.DBInfo.Host, "db-host", "mawinter-db", "DB Host")
startCmd.Flags().StringVar(&startOpt.DBInfo.Port, "db-port", "3306", "DB Port")
startCmd.Flags().StringVar(&startOpt.DBInfo.Name, "db-name", "mawinter", "DB Name")
startCmd.Flags().StringVar(&startOpt.DBInfo.User, "db-user", "root", "DB User")
startCmd.Flags().StringVar(&startOpt.DBInfo.Pass, "db-pass", "password", "DB Pass")
}
package api
import (
"context"
"fmt"
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"mawinter-server/internal/timeutil"
"time"
)
type mockRepo struct {
GetMonthlyFixDoneReturn bool
ReturnConfirm bool // ここが true ならば各テーブルは confirm していることにする
}
func (m *mockRepo) CreateTableYYYYMM(yyyymm string) (err error) {
return nil
}
func (m *mockRepo) InsertRecord(req openapi.ReqRecord) (rec openapi.Record, err error) {
rec = openapi.Record{
CategoryId: 100,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 23, 0, 0, 0, 0, jst),
From: "from",
Id: 1,
Memo: "memo",
Price: 1234,
Type: "type",
}
return rec, nil
}
// GetRecords は mock だと num <= 2 && offset = 0 と num = 20 && offset = 1 までしか対応しない
func (m *mockRepo) GetRecords(ctx context.Context, num int, offset int) (recs []openapi.Record, err error) {
if offset == 1 {
recs = []openapi.Record{
{
CategoryId: 200,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 25, 0, 0, 0, 0, jst),
From: "",
Id: 2,
Memo: "",
Price: 2345,
Type: "",
},
}
} else {
if num >= 2 {
recs = []openapi.Record{
{
CategoryId: 100,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 23, 0, 0, 0, 0, jst),
From: "from",
Id: 1,
Memo: "memo",
Price: 1234,
Type: "type",
},
{
CategoryId: 200,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 25, 0, 0, 0, 0, jst),
From: "",
Id: 2,
Memo: "",
Price: 2345,
Type: "",
},
}
} else if num == 1 {
recs = []openapi.Record{
{
CategoryId: 100,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 23, 0, 0, 0, 0, jst),
From: "from",
Id: 1,
Memo: "memo",
Price: 1234,
Type: "type",
},
}
} else if num == 0 {
recs = []openapi.Record{}
} else {
return []openapi.Record{}, fmt.Errorf("invalid args")
}
}
return recs, nil
}
func (m *mockRepo) GetRecordsCount(ctx context.Context) (num int, err error) {
return 123, nil // 正常系
}
// empty return
func (m *mockRepo) GetCategories(ctx context.Context) (cats []model.Category, err error) {
return []model.Category{}, nil
}
func (m *mockRepo) GetMonthRecords(yyyymm string) (recs []openapi.Record, err error) {
recs = []openapi.Record{
{
CategoryId: 100,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 23, 0, 0, 0, 0, jst),
From: "ope",
Id: 1,
Memo: "memo",
Price: 1234,
Type: "type",
},
{
CategoryId: 200,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 25, 0, 0, 0, 0, jst),
From: "mawinter-web",
Id: 2,
Memo: "",
Price: 2345,
Type: "",
},
}
return recs, nil
}
func (m *mockRepo) GetMonthRecordsRecent(yyyymm string, num int) (recs []openapi.Record, err error) {
recs = []openapi.Record{
{
CategoryId: 100,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 23, 0, 0, 0, 0, jst),
From: "from",
Id: 1,
Memo: "memo",
Price: 1234,
Type: "type",
},
{
CategoryId: 200,
// CategoryName string `json:"category_name"`
Datetime: time.Date(2000, 1, 25, 0, 0, 0, 0, jst),
From: "",
Id: 2,
Memo: "",
Price: 2345,
Type: "",
},
}
return recs, nil
}
func (m *mockRepo) MakeCategoryNameMap() (cnf map[int]string, err error) {
cnf = map[int]string{100: "cat1", 200: "cat2"}
return cnf, nil
}
func (m *mockRepo) GetMonthMidSummary(yyyymm string) (summon []model.CategoryMidMonthSummary, err error) {
// テストのため、200008 の場合のみ catID: 200 は ノーレコードとする。
if yyyymm == "200008" {
return []model.CategoryMidMonthSummary{
{
CategoryId: 100,
Count: 10,
Price: 1000,
},
}, nil
}
return []model.CategoryMidMonthSummary{
{
CategoryId: 100,
Count: 10,
Price: 1000,
},
{
CategoryId: 200,
Count: 20,
Price: 2000,
},
}, nil
}
func (m *mockRepo) InsertMonthlyFixBilling(yyyymm string) (recs []openapi.Record, err error) {
return []openapi.Record{
{
CategoryId: 100,
CategoryName: "cat1",
Datetime: time.Date(2021, 2, 15, 0, 0, 0, 0, jst),
From: "fixmonth",
Id: 1,
Memo: "",
Price: 1234,
Type: "",
},
{
CategoryId: 200,
CategoryName: "cat2",
Datetime: time.Date(2021, 2, 25, 0, 0, 0, 0, jst),
From: "fixmonth",
Id: 2,
Memo: "",
Price: 12345,
Type: "",
},
}, nil
}
func (m *mockRepo) GetMonthlyFixDone(yyyymm string) (done bool, err error) {
return m.GetMonthlyFixDoneReturn, nil
}
func (m *mockRepo) GetMonthlyConfirm(yyyymm string) (yc openapi.ConfirmInfo, err error) {
// 正常系
t := timeutil.NowFunc() // testconfig
return openapi.ConfirmInfo{
ConfirmDatetime: &t,
Status: &m.ReturnConfirm,
Yyyymm: &yyyymm,
}, nil
}
func (m *mockRepo) UpdateMonthlyConfirm(yyyymm string, confirm bool) (yc openapi.ConfirmInfo, err error) {
// 正常系
t := timeutil.NowFunc() // testconfig
return openapi.ConfirmInfo{
ConfirmDatetime: &t,
Status: &confirm,
Yyyymm: &yyyymm,
}, nil
}
package api
import (
"context"
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"mawinter-server/internal/timeutil"
"sort"
"time"
"go.uber.org/zap"
)
var jst *time.Location
func init() {
j, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
jst = j
}
type DBRepository interface {
InsertRecord(req openapi.ReqRecord) (rec openapi.Record, err error)
GetRecords(ctx context.Context, num int, offset int) (recs []openapi.Record, err error)
GetRecordsCount(ctx context.Context) (num int, err error)
GetCategories(ctx context.Context) (cats []model.Category, err error)
GetMonthRecords(yyyymm string) (recs []openapi.Record, err error)
GetMonthRecordsRecent(yyyymm string, num int) (recs []openapi.Record, err error)
MakeCategoryNameMap() (cnf map[int]string, err error)
GetMonthMidSummary(yyyymm string) (summon []model.CategoryMidMonthSummary, err error) // SELECT category_id, count(*), sum(price) FROM Record_202211 GROUP BY category_id;
InsertMonthlyFixBilling(yyyymm string) (recs []openapi.Record, err error)
GetMonthlyFixDone(yyyymm string) (done bool, err error)
GetMonthlyConfirm(yyyymm string) (yc openapi.ConfirmInfo, err error)
UpdateMonthlyConfirm(yyyymm string, confirm bool) (yc openapi.ConfirmInfo, err error)
}
type APIService struct {
Logger *zap.Logger
Repo DBRepository
}
func int2ptr(i int) *int {
return &i
}
func (a *APIService) PostRecord(ctx context.Context, req openapi.ReqRecord) (rec openapi.Record, err error) {
a.Logger.Info("called post record")
a.Logger.Info("get monthly confirm")
// 確定した月でないかを確認する
// FIX: req.Datetime の変換タイミングが悪いので暫定対応
var yyyymm string
if req.Datetime == nil {
// Datetime が未設定なら現時刻がDBに挿入されるはずなので、今の時点でのYYYYMMをセットする
yyyymm = timeutil.NowFunc().Format("200601")
} else {
yyyymm = (*req.Datetime)[0:6]
}
yc, err := a.Repo.GetMonthlyConfirm(yyyymm)
if err != nil {
return openapi.Record{}, err
}
if *yc.Status {
a.Logger.Info("already confirm month", zap.String("yyyymm", yyyymm))
return openapi.Record{}, model.ErrAlreadyRecorded
}
a.Logger.Info("get category name mapping")
// categoryNameMap 取得
cnf, err := a.Repo.MakeCategoryNameMap()
if err != nil {
return openapi.Record{}, err
}
_, ok := cnf[req.CategoryId] // DBにCategory IDがあるか確認
if !ok {
// Category ID がDBに未登録の場合
a.Logger.Warn("unknown category ID", zap.Int("category_id", rec.CategoryId))
return openapi.Record{}, model.ErrUnknownCategoryID
}
rec, err = a.Repo.InsertRecord(req)
if err != nil {
a.Logger.Error("failed to insert", zap.String("msg", err.Error()), zap.Error(err))
return openapi.Record{}, err
}
rec.CategoryName = cnf[rec.CategoryId]
a.Logger.Info("complete post record")
return rec, nil
}
// GetRecords は num の数だけ ID 降順に Record を取得する
func (a *APIService) GetRecords(ctx context.Context, num int, offset int) (recs []openapi.Record, err error) {
a.Logger.Info("called GetRecordsRecent", zap.Int("num", num))
recsRaw, err := a.Repo.GetRecords(ctx, num, offset)
if err != nil {
a.Logger.Error("failed to get records")
return []openapi.Record{}, err
}
a.Logger.Info("get category name mapping")
// categoryNameMap 取得
cnf, err := a.Repo.MakeCategoryNameMap()
if err != nil {
return nil, err
}
for _, rec := range recsRaw {
// categoryName を付与
rec.CategoryName = cnf[rec.CategoryId]
recs = append(recs, rec)
}
a.Logger.Info("complete GetRecordsRecent", zap.Int("num", num))
return recs, nil
}
// GetRecordsCount は レコード件数の総数を返す
func (a *APIService) GetRecordsCount(ctx context.Context) (rec openapi.RecordCount, err error) {
a.Logger.Info("called GetRecordsCount")
num, err := a.Repo.GetRecordsCount(ctx)
if err != nil {
a.Logger.Error("failed to get the number of records", zap.Error(err))
return openapi.RecordCount{}, err
}
rec.Num = int2ptr(num)
return rec, nil
}
// GetCategories は管理しているカテゴリ情報を返却する
func (a *APIService) GetCategories(ctx context.Context) (cats []openapi.Category, err error) {
a.Logger.Info("called get categories")
cs, err := a.Repo.GetCategories(ctx)
if err != nil {
a.Logger.Error("failed to get categories from DB", zap.Error(err))
return []openapi.Category{}, err
}
for _, c := range cs {
var tmpC openapi.Category
tmpC.CategoryId = int(c.CategoryID)
tmpC.CategoryName = c.Name
cats = append(cats, tmpC)
}
return cats, nil
}
func (a *APIService) PostMonthlyFixRecord(ctx context.Context, yyyymm string) (recs []openapi.Record, err error) {
a.Logger.Info("called post fixmonth records", zap.String("yyyymm", yyyymm))
done, err := a.Repo.GetMonthlyFixDone(yyyymm)
if err != nil {
a.Logger.Error("failed to get monthly processed data", zap.String("yyyymm", yyyymm), zap.Error(err))
return []openapi.Record{}, err
}
if done {
// 既に処理済の場合はスキップ
a.Logger.Info("called post monthly already registed", zap.String("yyyymm", yyyymm))
return []openapi.Record{}, model.ErrAlreadyRecorded
}
recs, err = a.Repo.InsertMonthlyFixBilling(yyyymm)
if err != nil {
a.Logger.Error("failed to insert data", zap.String("yyyymm", yyyymm), zap.Error(err))
return []openapi.Record{}, err
}
a.Logger.Info("complete post fixmonth record", zap.String("yyyymm", yyyymm))
return recs, nil
}
// FYyyyy の yyyymm をリストで返す
func fyInterval(yyyy int) (yyyymm []string) {
t := time.Date(yyyy, 4, 1, 0, 0, 0, 0, jst)
for i := 0; i < 12; i++ {
yyyymm = append(yyyymm, t.Format("200601"))
t = t.AddDate(0, 1, 0)
}
return yyyymm
}
// GetYYYYMMRecords は yyyymm 月のレコードを取得する
func (a *APIService) GetYYYYMMRecords(ctx context.Context, yyyymm string, params openapi.GetV2RecordYyyymmParams) (recs []openapi.Record, err error) {
recs = []openapi.Record{}
a.Logger.Info("called get month records")
a.Logger.Info("get category name mapping")
// categoryNameMap 取得
cnf, err := a.Repo.MakeCategoryNameMap()
if err != nil {
return nil, err
}
a.Logger.Info("get records from DB")
recsRaw, err := a.Repo.GetMonthRecords(yyyymm) // category_name なし
if err != nil {
return nil, err
}
// parameters 抽出する
// category_id
var recRawExt1 []openapi.Record // category_id でフィルタリングしたもの
var recRawExt2 []openapi.Record // from でフィルタリングしたもの
for _, r := range recsRaw {
if params.CategoryId == nil || (r.CategoryId == *params.CategoryId) {
recRawExt1 = append(recRawExt1, r)
}
}
// from
for _, r := range recRawExt1 {
if params.From == nil || (r.From == *params.From) {
recRawExt2 = append(recRawExt2, r)
}
}
for _, rec := range recRawExt2 {
// categoryName を付与
rec.CategoryName = cnf[rec.CategoryId]
recs = append(recs, rec)
}
a.Logger.Info("complete get month records")
return recs, nil
}
func (a *APIService) GetYYYYMMRecordsRecent(ctx context.Context, yyyymm string, num int) (recs []openapi.Record, err error) {
recs = []openapi.Record{}
a.Logger.Info("called get month recent records")
a.Logger.Info("get records from DB")
recsRaw, err := a.Repo.GetMonthRecordsRecent(yyyymm, num)
if err != nil {
return nil, err
}
a.Logger.Info("get category name mapping")
// categoryNameMap 取得
cnf, err := a.Repo.MakeCategoryNameMap()
if err != nil {
return nil, err
}
for _, rec := range recsRaw {
// categoryName を付与
rec.CategoryName = cnf[rec.CategoryId]
recs = append(recs, rec)
}
a.Logger.Info("complete get month recent records")
return recs, nil
}
func (a *APIService) GetV2YearSummary(ctx context.Context, year int) (sums []openapi.CategoryYearSummary, err error) {
a.Logger.Info("called get year summary")
a.Logger.Info("get category name mapping")
// categoryNameMap 取得
cnf, err := a.Repo.MakeCategoryNameMap()
if err != nil {
return nil, err
}
sumsDec := make(map[int]*openapi.CategoryYearSummary) // CatID -> openapi.CategoryYearSummary
// 初期化
for catId := range cnf {
sumsDec[catId] = &openapi.CategoryYearSummary{
CategoryId: catId,
CategoryName: cnf[catId],
Count: 0,
Price: make([]int, 12),
Total: 0,
}
}
a.Logger.Info("get records from DB")
// 1月ずつ処理する
yyyymmList := fyInterval(year)
for mi, yyyymm := range yyyymmList {
monthSums, err := a.Repo.GetMonthMidSummary(yyyymm)
if err != nil {
a.Logger.Error("failed to get info from DB", zap.Error(err))
return nil, err
}
for _, monthSum := range monthSums {
catId := monthSum.CategoryId
count := monthSum.Count
price := monthSum.Price
sumsDec[catId].Count += count
sumsDec[catId].Price[mi] = price
sumsDec[catId].Total += price
}
}
a.Logger.Info("making month summary")
// 最終的に出力する構造体に挿入する
for _, v := range sumsDec {
newSum := openapi.CategoryYearSummary{
CategoryId: v.CategoryId,
CategoryName: v.CategoryName,
Count: v.Count,
Price: v.Price,
Total: v.Total,
}
sums = append(sums, newSum)
}
sort.Slice(sums, func(i, j int) bool {
return sums[i].CategoryId < sums[j].CategoryId
})
a.Logger.Info("complete get year summary")
return sums, nil
}
func (a *APIService) GetMonthlyConfirm(ctx context.Context, yyyymm string) (yc openapi.ConfirmInfo, err error) {
a.Logger.Info("called GetMonthlyConfirm")
yc.Yyyymm = &yyyymm
yc, err = a.Repo.GetMonthlyConfirm(yyyymm)
if err != nil {
// Internal error -> error
a.Logger.Error("failed to get monthly confirm", zap.Error(err))
return openapi.ConfirmInfo{}, err
} else {
// success fetch data or not found (= false)
a.Logger.Info("fetch monthly confirm successfully", zap.Error(err))
}
a.Logger.Info("complete GetMonthlyConfirm")
return yc, nil
}
func (a *APIService) UpdateMonthlyConfirm(ctx context.Context, yyyymm string, confirm bool) (yc openapi.ConfirmInfo, err error) {
a.Logger.Info("called UpdateMonthlyConfirm")
yc.Yyyymm = &yyyymm
yc, err = a.Repo.UpdateMonthlyConfirm(yyyymm, confirm)
if err != nil {
// Internal error -> error
a.Logger.Error("failed to get monthly confirm", zap.Error(err))
return openapi.ConfirmInfo{}, err
}
a.Logger.Info("update monthly confirm successfully", zap.Error(err))
a.Logger.Info("complete UpdateMonthlyConfirm")
return yc, nil
}
package client
import (
"context"
"fmt"
"net/smtp"
)
type MailClient struct {
SMTPHost string
SMTPPort string
SMTPUser string
SMTPPass string
}
func (m *MailClient) Send(ctx context.Context, to string, title string, body string) (err error) {
recipients := []string{to}
from := m.SMTPUser
auth := smtp.PlainAuth("", m.SMTPUser, m.SMTPPass, m.SMTPHost)
// 送信先は1つのみ対応
msg := []byte("To: " + to + "\r\n" + "Subject:" + title + "\r\n" + "\r\n" + body)
if err := smtp.SendMail(fmt.Sprintf("%s:%s", m.SMTPHost, m.SMTPPort), auth, from, recipients, msg); err != nil {
return err
}
return nil
}
package factory
import (
"net"
"time"
v2db "mawinter-server/internal/repository/v2"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
const DBConnectRetry = 5
const DBConnectRetryInterval = 10
func NewDBRepositoryV1(host, port, user, pass, name string) (dbR *v2db.DBRepository, err error) {
l, err := NewLogger()
if err != nil {
return nil, err
}
addr := net.JoinHostPort(host, port)
dsn := user + ":" + pass + "@(" + addr + ")/" + name + "?parseTime=true&loc=Local"
var gormdb *gorm.DB
for i := 0; i < DBConnectRetry; i++ {
gormdb, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err == nil {
// Success DB connect
l.Info("DB connect")
break
}
l.Warn("DB connection retry")
if i == DBConnectRetry {
l.Error("failed to connect DB", zap.Error(err))
return nil, err
}
time.Sleep(DBConnectRetryInterval * time.Second)
}
return &v2db.DBRepository{Conn: gormdb}, nil
}
func NewDBRepositoryV2(host, port, user, pass, name string) (dbR *v2db.DBRepository, err error) {
l, err := NewLogger()
if err != nil {
return nil, err
}
addr := net.JoinHostPort(host, port)
dsn := user + ":" + pass + "@(" + addr + ")/" + name + "?parseTime=true&loc=Local"
var gormdb *gorm.DB
for i := 0; i < DBConnectRetry; i++ {
gormdb, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err == nil {
// Success DB connect
l.Info("DB connect")
break
}
l.Warn("DB connection retry")
if i == DBConnectRetry {
l.Error("failed to connect DB", zap.Error(err))
return nil, err
}
time.Sleep(DBConnectRetryInterval * time.Second)
}
return &v2db.DBRepository{Conn: gormdb}, nil
}
package factory
import (
"fmt"
v2 "mawinter-server/internal/api/v2"
"mawinter-server/internal/client"
"mawinter-server/internal/register"
v2db "mawinter-server/internal/repository/v2"
"mawinter-server/internal/server"
"mawinter-server/internal/service"
"os"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func NewLogger() (*zap.Logger, error) {
config := zap.NewProductionConfig()
// config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
config.EncoderConfig.EncodeTime = JSTTimeEncoder
l, err := config.Build()
l.WithOptions(zap.AddStacktrace(zap.ErrorLevel))
if err != nil {
fmt.Printf("failed to create logger: %v\n", err)
}
return l, err
}
func JSTTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
const layout = "2006-01-02T15:04:05+09:00"
jst := time.FixedZone("Asia/Tokyo", 9*60*60)
enc.AppendString(t.In(jst).Format(layout))
}
func NewServiceV2(l *zap.Logger, db *v2db.DBRepository) (ap *v2.APIService) {
return &v2.APIService{Logger: l, Repo: db}
}
func NewRegisterService(l *zap.Logger, db *v2db.DBRepository, mc *client.MailClient) (ap *register.RegisterService) {
return ®ister.RegisterService{Logger: l, DB: db, MailClient: mc}
}
func NewServer(l *zap.Logger, ap2 *v2.APIService) *server.Server {
return &server.Server{Logger: l, Ap2: ap2, BasicAuth: struct {
User string
Pass string
}{os.Getenv("BASIC_AUTH_USERNAME"), os.Getenv("BASIC_AUTH_PASSWORD")}}
}
func NewMailClient() *client.MailClient {
host := os.Getenv("MAIL_HOST")
port := os.Getenv("MAIL_PORT")
user := os.Getenv("MAIL_USER")
pass := os.Getenv("MAIL_PASS")
return &client.MailClient{
SMTPHost: host,
SMTPPort: port,
SMTPUser: user,
SMTPPass: pass,
}
}
func NewDuplicateCheckService(l *zap.Logger, ap *v2.APIService) (svc *service.DuplicateCheckService) {
return &service.DuplicateCheckService{Logger: l, Ap: ap, MailClient: NewMailClient()}
}
package model
import (
"time"
)
func (Category) TableName() string {
return "Category"
}
func (MonthlyFixBillingDB) TableName() string {
return "Monthly_Fix_Billing"
}
func (MonthlyFixDoneDB) TableName() string {
return "Monthly_Fix_Done"
}
type Category struct {
ID int64 `gorm:"id"`
CategoryID int64 `gorm:"column:category_id"`
Name string `gorm:"column:name"`
}
type Record struct {
ID int64 `gorm:"id;primaryKey"`
CategoryID int64 `gorm:"column:category_id;index;not null"`
Datetime time.Time `gorm:"column:datetime;autoCreateTime;index;not null"`
Price int64 `gorm:"column:price"`
From string `gorm:"column:from"`
Type string `gorm:"column:type"`
Memo string `gorm:"column:memo"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
type MonthlyFixBillingDB struct {
ID int64 `gorm:"id;primaryKey"`
CategoryID int64 `gorm:"column:category_id"`
Day int64 `gorm:"column:day"`
Price int64 `gorm:"column:price"`
Type string `gorm:"column:type"`
Memo string `gorm:"column:memo"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
type MonthlyFixDoneDB struct {
YYYYMM string `gorm:"column:yyyymm;primaryKey"`
Done uint8 `gorm:"column:done"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
// SumPriceCategoryID uses v1.
type SumPriceCategoryID struct {
CategoryID int64
Count int64
Sum int64
}
type MonthlyConfirm struct {
YYYYMM string `gorm:"column:yyyymm;primaryKey"`
Confirm uint8 `gorm:"column:confirm"`
ConfirmDatetime time.Time `gorm:"column:confirm_datetime"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
package model
import (
"fmt"
"mawinter-server/internal/openapi"
"strconv"
"time"
)
var jst *time.Location
func init() {
j, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
jst = j
}
type BillAPIResponse struct {
BillName string `json:"bill_name"`
Price int `json:"price"`
}
func (b *BillAPIResponse) NewRecordstruct() (req openapi.Record, err error) {
req = openapi.Record{
Datetime: time.Now().Local(),
From: "bill-manager-api",
Price: b.Price,
}
switch b.BillName {
case "elect":
req.CategoryId = 220
case "gas":
req.CategoryId = 221
case "water":
req.CategoryId = 222
default:
return openapi.Record{}, fmt.Errorf("unknown billname")
}
return req, nil
}
func NewMailMonthlyFixBilling(recs []openapi.Record) (text string) {
for _, rec := range recs {
text += fmt.Sprintf("%v,%v,%v,%v\n", rec.CategoryId, rec.Price, rec.Type, rec.Memo)
}
return text
}
func NewMailMonthlyRegistBill(ress []BillAPIResponse) (text string) {
for _, res := range ress {
text += fmt.Sprintf("%s,%d\n", res.BillName, res.Price)
}
return text
}
type MonthlyFixBilling struct {
CategoryID int
Day int
Price int
Type string
Memo string
}
func (m *MonthlyFixBilling) ConvAddDBModel(yyyymm string) (Record, error) {
yyyynum, err := strconv.Atoi(yyyymm[0:4])
if err != nil {
return Record{}, err
}
mmnum, err := strconv.Atoi(yyyymm[5:6])
if err != nil {
return Record{}, err
}
return Record{
CategoryID: int64(m.CategoryID),
Datetime: time.Date(yyyynum, time.Month(mmnum), m.Day, 0, 0, 0, 0, jst),
From: "fixmonth", // 固定値
Price: int64(m.Price),
Type: m.Type,
Memo: m.Memo,
}, nil
}
package model
import (
"fmt"
"strconv"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
)
func ValidYYYY(yyyy string) (yyyyint int, err error) {
if err := validation.Validate(yyyy, validation.Length(4, 4), is.Digit); err != nil {
return 0, fmt.Errorf("invalid YYYY: %w", ErrInvalidValue)
}
yyyyint, err = strconv.Atoi(yyyy)
if err != nil {
return 0, err
}
return yyyyint, err
}
func ValidYYYYMM(yyyymm string) (err error) {
if err := validation.Validate(yyyymm, validation.Length(6, 6), is.Digit); err != nil {
return fmt.Errorf("invalid YYYYMM")
}
return nil
}
// Package openapi provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.15.0 DO NOT EDIT.
package openapi
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/oapi-codegen/runtime"
)
// ServerInterface represents all server handlers.
type ServerInterface interface {
// health check
// (GET /)
Get(w http.ResponseWriter, r *http.Request)
// Your GET endpoint
// (GET /categories)
GetCategories(w http.ResponseWriter, r *http.Request)
// get records
// (GET /v2/record)
GetV2Record(w http.ResponseWriter, r *http.Request, params GetV2RecordParams)
// create record
// (POST /v2/record)
PostV2Record(w http.ResponseWriter, r *http.Request)
// /v2/record/count
// (GET /v2/record/count)
GetV2RecordCount(w http.ResponseWriter, r *http.Request)
// create fixmonth record
// (POST /v2/record/fixmonth)
PostV2RecordFixmonth(w http.ResponseWriter, r *http.Request, params PostV2RecordFixmonthParams)
// get year summary
// (GET /v2/record/summary/{year})
GetV2RecordYear(w http.ResponseWriter, r *http.Request, year int)
// get month records
// (GET /v2/record/{yyyymm})
GetV2RecordYyyymm(w http.ResponseWriter, r *http.Request, yyyymm string, params GetV2RecordYyyymmParams)
// (GET /v2/record/{yyyymm}/confirm)
GetV2RecordYyyymmConfirm(w http.ResponseWriter, r *http.Request, yyyymm string)
// (PUT /v2/record/{yyyymm}/confirm)
PutV2TableYyyymmConfirm(w http.ResponseWriter, r *http.Request, yyyymm string)
// Your GET endpoint
// (GET /v2/record/{yyyymm}/recent)
GetV2RecordYyyymmRecent(w http.ResponseWriter, r *http.Request, yyyymm string, params GetV2RecordYyyymmRecentParams)
// get version
// (GET /version)
GetVersion(w http.ResponseWriter, r *http.Request)
}
// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint.
type Unimplemented struct{}
// health check
// (GET /)
func (_ Unimplemented) Get(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Your GET endpoint
// (GET /categories)
func (_ Unimplemented) GetCategories(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// get records
// (GET /v2/record)
func (_ Unimplemented) GetV2Record(w http.ResponseWriter, r *http.Request, params GetV2RecordParams) {
w.WriteHeader(http.StatusNotImplemented)
}
// create record
// (POST /v2/record)
func (_ Unimplemented) PostV2Record(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// /v2/record/count
// (GET /v2/record/count)
func (_ Unimplemented) GetV2RecordCount(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// create fixmonth record
// (POST /v2/record/fixmonth)
func (_ Unimplemented) PostV2RecordFixmonth(w http.ResponseWriter, r *http.Request, params PostV2RecordFixmonthParams) {
w.WriteHeader(http.StatusNotImplemented)
}
// get year summary
// (GET /v2/record/summary/{year})
func (_ Unimplemented) GetV2RecordYear(w http.ResponseWriter, r *http.Request, year int) {
w.WriteHeader(http.StatusNotImplemented)
}
// get month records
// (GET /v2/record/{yyyymm})
func (_ Unimplemented) GetV2RecordYyyymm(w http.ResponseWriter, r *http.Request, yyyymm string, params GetV2RecordYyyymmParams) {
w.WriteHeader(http.StatusNotImplemented)
}
// (GET /v2/record/{yyyymm}/confirm)
func (_ Unimplemented) GetV2RecordYyyymmConfirm(w http.ResponseWriter, r *http.Request, yyyymm string) {
w.WriteHeader(http.StatusNotImplemented)
}
// (PUT /v2/record/{yyyymm}/confirm)
func (_ Unimplemented) PutV2TableYyyymmConfirm(w http.ResponseWriter, r *http.Request, yyyymm string) {
w.WriteHeader(http.StatusNotImplemented)
}
// Your GET endpoint
// (GET /v2/record/{yyyymm}/recent)
func (_ Unimplemented) GetV2RecordYyyymmRecent(w http.ResponseWriter, r *http.Request, yyyymm string, params GetV2RecordYyyymmRecentParams) {
w.WriteHeader(http.StatusNotImplemented)
}
// get version
// (GET /version)
func (_ Unimplemented) GetVersion(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
type MiddlewareFunc func(http.Handler) http.Handler
// Get operation middleware
func (siw *ServerInterfaceWrapper) Get(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.Get(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetCategories operation middleware
func (siw *ServerInterfaceWrapper) GetCategories(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetCategories(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetV2Record operation middleware
func (siw *ServerInterfaceWrapper) GetV2Record(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params GetV2RecordParams
// ------------- Optional query parameter "num" -------------
err = runtime.BindQueryParameter("form", true, false, "num", r.URL.Query(), ¶ms.Num)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "num", Err: err})
return
}
// ------------- Optional query parameter "offset" -------------
err = runtime.BindQueryParameter("form", true, false, "offset", r.URL.Query(), ¶ms.Offset)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetV2Record(w, r, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// PostV2Record operation middleware
func (siw *ServerInterfaceWrapper) PostV2Record(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.PostV2Record(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetV2RecordCount operation middleware
func (siw *ServerInterfaceWrapper) GetV2RecordCount(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetV2RecordCount(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// PostV2RecordFixmonth operation middleware
func (siw *ServerInterfaceWrapper) PostV2RecordFixmonth(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params PostV2RecordFixmonthParams
// ------------- Optional query parameter "yyyymm" -------------
err = runtime.BindQueryParameter("form", true, false, "yyyymm", r.URL.Query(), ¶ms.Yyyymm)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "yyyymm", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.PostV2RecordFixmonth(w, r, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetV2RecordYear operation middleware
func (siw *ServerInterfaceWrapper) GetV2RecordYear(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// ------------- Path parameter "year" -------------
var year int
err = runtime.BindStyledParameterWithLocation("simple", false, "year", runtime.ParamLocationPath, chi.URLParam(r, "year"), &year)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "year", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetV2RecordYear(w, r, year)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetV2RecordYyyymm operation middleware
func (siw *ServerInterfaceWrapper) GetV2RecordYyyymm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// ------------- Path parameter "yyyymm" -------------
var yyyymm string
err = runtime.BindStyledParameterWithLocation("simple", false, "yyyymm", runtime.ParamLocationPath, chi.URLParam(r, "yyyymm"), &yyyymm)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "yyyymm", Err: err})
return
}
// Parameter object where we will unmarshal all parameters from the context
var params GetV2RecordYyyymmParams
// ------------- Optional query parameter "category_id" -------------
err = runtime.BindQueryParameter("form", true, false, "category_id", r.URL.Query(), ¶ms.CategoryId)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "category_id", Err: err})
return
}
// ------------- Optional query parameter "from" -------------
err = runtime.BindQueryParameter("form", true, false, "from", r.URL.Query(), ¶ms.From)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "from", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetV2RecordYyyymm(w, r, yyyymm, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetV2RecordYyyymmConfirm operation middleware
func (siw *ServerInterfaceWrapper) GetV2RecordYyyymmConfirm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// ------------- Path parameter "yyyymm" -------------
var yyyymm string
err = runtime.BindStyledParameterWithLocation("simple", false, "yyyymm", runtime.ParamLocationPath, chi.URLParam(r, "yyyymm"), &yyyymm)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "yyyymm", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetV2RecordYyyymmConfirm(w, r, yyyymm)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// PutV2TableYyyymmConfirm operation middleware
func (siw *ServerInterfaceWrapper) PutV2TableYyyymmConfirm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// ------------- Path parameter "yyyymm" -------------
var yyyymm string
err = runtime.BindStyledParameterWithLocation("simple", false, "yyyymm", runtime.ParamLocationPath, chi.URLParam(r, "yyyymm"), &yyyymm)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "yyyymm", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.PutV2TableYyyymmConfirm(w, r, yyyymm)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetV2RecordYyyymmRecent operation middleware
func (siw *ServerInterfaceWrapper) GetV2RecordYyyymmRecent(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// ------------- Path parameter "yyyymm" -------------
var yyyymm string
err = runtime.BindStyledParameterWithLocation("simple", false, "yyyymm", runtime.ParamLocationPath, chi.URLParam(r, "yyyymm"), &yyyymm)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "yyyymm", Err: err})
return
}
// Parameter object where we will unmarshal all parameters from the context
var params GetV2RecordYyyymmRecentParams
// ------------- Optional query parameter "num" -------------
err = runtime.BindQueryParameter("form", true, false, "num", r.URL.Query(), ¶ms.Num)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "num", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetV2RecordYyyymmRecent(w, r, yyyymm, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetVersion operation middleware
func (siw *ServerInterfaceWrapper) GetVersion(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetVersion(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
type UnescapedCookieParamError struct {
ParamName string
Err error
}
func (e *UnescapedCookieParamError) Error() string {
return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName)
}
func (e *UnescapedCookieParamError) Unwrap() error {
return e.Err
}
type UnmarshalingParamError struct {
ParamName string
Err error
}
func (e *UnmarshalingParamError) Error() string {
return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error())
}
func (e *UnmarshalingParamError) Unwrap() error {
return e.Err
}
type RequiredParamError struct {
ParamName string
}
func (e *RequiredParamError) Error() string {
return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName)
}
type RequiredHeaderError struct {
ParamName string
Err error
}
func (e *RequiredHeaderError) Error() string {
return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName)
}
func (e *RequiredHeaderError) Unwrap() error {
return e.Err
}
type InvalidParamFormatError struct {
ParamName string
Err error
}
func (e *InvalidParamFormatError) Error() string {
return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error())
}
func (e *InvalidParamFormatError) Unwrap() error {
return e.Err
}
type TooManyValuesForParamError struct {
ParamName string
Count int
}
func (e *TooManyValuesForParamError) Error() string {
return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count)
}
// Handler creates http.Handler with routing matching OpenAPI spec.
func Handler(si ServerInterface) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{})
}
type ChiServerOptions struct {
BaseURL string
BaseRouter chi.Router
Middlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseRouter: r,
})
}
func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseURL: baseURL,
BaseRouter: r,
})
}
// HandlerWithOptions creates http.Handler with additional options
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
r := options.BaseRouter
if r == nil {
r = chi.NewRouter()
}
if options.ErrorHandlerFunc == nil {
options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
ErrorHandlerFunc: options.ErrorHandlerFunc,
}
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/", wrapper.Get)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/categories", wrapper.GetCategories)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/v2/record", wrapper.GetV2Record)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/v2/record", wrapper.PostV2Record)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/v2/record/count", wrapper.GetV2RecordCount)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/v2/record/fixmonth", wrapper.PostV2RecordFixmonth)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/v2/record/summary/{year}", wrapper.GetV2RecordYear)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/v2/record/{yyyymm}", wrapper.GetV2RecordYyyymm)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/v2/record/{yyyymm}/confirm", wrapper.GetV2RecordYyyymmConfirm)
})
r.Group(func(r chi.Router) {
r.Put(options.BaseURL+"/v2/record/{yyyymm}/confirm", wrapper.PutV2TableYyyymmConfirm)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/v2/record/{yyyymm}/recent", wrapper.GetV2RecordYyyymmRecent)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/version", wrapper.GetVersion)
})
return r
}
// Package openapi provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.15.0 DO NOT EDIT.
package openapi
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"net/url"
"path"
"strings"
"github.com/getkin/kin-openapi/openapi3"
)
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/9RZ7W7URhe+FWveV+KPw3odVFH/JAWEKrUIISQUomhiz2ZNbY8Zj0NWq5XwbqFfoKqp",
"gNIKBBLio2nTihakkLZczGT5uItq7LHXXo93s0k2qD+IdndmzjznPM85c2ZoAxO7PvaQRwNgtEFgNpEL",
"448mpGgZkxb/7BPsI0JtVBhZtC3+lbZ8BAxgexQtIwI66mCCB12UmxJQYnvLoNNRAUGXQpsgCxjzBXvD",
"ixdUQG3q8NUZIDW1h5cuIpMWdmwhSBaD0HVhghytQtd3OO75IeBaCWeKTwUmDj0aT/GJbSJgzM9qmjrB",
"Pw4bU+gAQ+ssqPsdvwygbLFA3AY2RW68n4UaMHQSf3LxEDBLBly4eipZWtdV4Npe7puYDQmBLT5XOFnG",
"MQHFqTcp9NSqhPoivTIdYK9hE3fR9hpYIlwxakGKqJ2EtoGJCykwAP9xJv5VLcc7oJCGQc7RJYwdBD0+",
"1mq1Wq4rl3nmQB6XBDdBJibWkGCnIZzJXW8Q7EpNVYFwkYulCzJlltckv4wrFEXxSKWU+SeAC9MDbcXo",
"ctISka/kZDFLtSIXXuhW6L5oeTEVt8T+pUUp70NE13UtzxvQNX1Wq2v11EUDWHYgnEhiDzJ/Db2uZVuD",
"XZSivF5yEIGuaR9odU3nofQhpYh4wADz2syHCxOJaHpiKeokMVfgPYt+iRtuKq0g6XwXXuYQyAz0baCC",
"FUQCG3Of64c1Dgr7yONDBpg9zH+Kw9KMg1zjf5YRTWpxYBLbp8naJoIObSpmE5mfXfCgcxm2AoUgGhJP",
"OXSKKnag0CZSCMZU8eEyOgTijQjk609ZwAAnEdcWQYGPvSDmlFcrRDi+WEohcfhGlPpGreZgEzpNHFDj",
"qHZUi/WQnZUFMHE0ayKEQivCg9L+c4NZRSRcJKLsUpTkEPR9xzbj5bWLAY9B2nMUTqz/E9QABvhfbdCd",
"1ERrUsvagM7wccQxF+P76cexI7uLx3kcEuXk8bMK8iwf2x5NgrKi1wZZK2XVtli08e7OjXf3r7Fo/Uw8",
"mXXX+t/e6v9zm0V3WPcbdqV7wWO9L1jvJus+Zb111vuSRY9Z9JuubW+9GJoqY/2cfiZVrw8JdBFNPSyC",
"4frxQncJEQU3lAQ5J8rmg5dCFJ+jov/hNU3N8ZE1DrqmSipdW2oENxpBLMqBndLShSRVUUCPYatV1EiS",
"x9NWkaBw2hpaRjSLOS9qOJDohfV+Yd0/WO8v1vuKddfqLHr49tXf/a/vJwIosX8aB3n6KyM5Jk6jw5NV",
"x05Hwkh9H3cSu6gFI6uuswsbJfLmCIIUWXtg0IwtKDmYgwpQy3oDaR3YfnW3/+sPLLrJutdZ9IhFn/N0",
"zpMdbfSvPtneevH65u+su/b2wZM3D1/uKOvnRFOxi0QZnOJtcDz5rMR0rkAnRFlrU9dnj8QBnYQC0ezs",
"dxaVIj5EQ8NedbFHm3GXJk2xj46xaP3Nna1315+x6HZGRv+nl/2NH98+22K9azElt3gpjjb61672NzZZ",
"d41diZKAKyxa33FSnkjxlGqzrGCK+8P4grmHDNznmpillQp07Ug53J9gZU5A23PmpeRWpKCYX2vzq2Gn",
"Mhf5qNLf/LP/8hGLNlj3OevdY72fOenFxBuVdecRJBWc8mYvR2kycdCOUhKiCQmeYutUvEbv8AxUwRE5",
"1VQ5gUPP2uMpGROUQSpy3E5SZAS78bjCmS2epZzTaJNFj4ZqK+t9x7qb/G/vKes9iGXwikWPX999sL31",
"gnXX3jy/x2dG37PoCYuux73ZjfF1+XyayzvI++IFZYQ2Khotcb8tLRzch+TaTCGOVWdmaOG9dmQqcEOH",
"2j4ktNbAxJ2xIIX70RwcgKrzlSuoknVNvA6NumQVBTYnFuyoFu2S72m15yMLVP6Z7GAY4115KIn56ZCe",
"08/CJQe995CPCWjxRaf6pbIjeec4QDLH9PeSJ5jpc1+RjwSZqNDXj0nHM8n8MTdxF66KSiAu5Du8hVee",
"B//xun7Q9bjqPSd9yRvBtpiyXzm6FNqOJX31JGjFTtGUBnNIJf/TMCa193SMpTt3JsyvfwMAAP//MW6J",
"wF8cAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
res := make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
resolvePath := PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
pathToFile := url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}
package register
import (
"context"
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"time"
)
type mockRepo struct {
err error
monthlyFixDone bool
errGetMonthly error
}
type mockMailClient struct {
err error
}
func (m *mockRepo) InsertUniqueCatIDRecord(req openapi.Record) (res openapi.Record, err error) {
if m.err != nil {
return openapi.Record{}, m.err
}
return openapi.Record{
Id: 1,
CategoryId: 210,
Datetime: time.Now(),
From: "bill-manager-api",
Type: "",
Price: 1234,
}, nil
}
func (m *mockRepo) GetMonthlyFixDone(yyyymm string) (flag bool, err error) {
if m.errGetMonthly != nil {
return false, m.errGetMonthly
}
return m.monthlyFixDone, nil
}
func (m *mockRepo) GetMonthlyFixBilling() (fixBills []model.MonthlyFixBilling, err error) {
if m.errGetMonthly != nil {
return []model.MonthlyFixBilling{}, m.errGetMonthly
}
return []model.MonthlyFixBilling{
{
CategoryID: 100,
Day: 2,
Type: "type1",
Memo: "memo1",
},
{
CategoryID: 101,
Day: 4,
Type: "type2",
Memo: "memo2",
},
}, nil
}
func (m *mockRepo) InsertMonthlyFixBilling(yyyymm string) (recs []openapi.Record, err error) {
return []openapi.Record{}, m.errGetMonthly
}
func (m *mockMailClient) Send(ctx context.Context, to string, title string, body string) (err error) {
if m.err != nil {
return m.err
}
return nil
}
package register
import (
"context"
"errors"
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"os"
"go.uber.org/zap"
)
var (
ErrAlreadyRegisted = errors.New("already registed")
)
type DBRepository interface {
InsertUniqueCatIDRecord(req openapi.Record) (res openapi.Record, err error)
GetMonthlyFixDone(yyyymm string) (flag bool, err error)
GetMonthlyFixBilling() (fixBills []model.MonthlyFixBilling, err error)
InsertMonthlyFixBilling(yyyymm string) (recs []openapi.Record, err error)
}
type MailClient interface {
Send(ctx context.Context, to string, title string, body string) (err error)
}
type RegisterService struct {
Logger *zap.Logger
DB DBRepository
MailClient MailClient
}
// InsertMonthlyFixBilling は 固定費を登録する
func (r *RegisterService) InsertMonthlyFixBilling(ctx context.Context, yyyymm string) (err error) {
// すでに処理済なら skip
done, err := r.DB.GetMonthlyFixDone(yyyymm)
if err != nil {
r.Logger.Error("failed to get done status", zap.Error(err))
return err
}
if done {
r.Logger.Warn("this month is processed")
return model.ErrAlreadyRecorded
}
lg := r.Logger.With(zap.String("yyyymm", yyyymm))
// Insert
recs, err := r.DB.InsertMonthlyFixBilling(yyyymm)
if err != nil {
lg.Error("failed to insert fix billing records", zap.Error(err))
return err
}
lg.Info("insert fix billing records to DB")
// 環境変数 MAIL_TO に何か入ったときのみ通知メールを送信する。
if os.Getenv("MAIL_TO") != "" {
err = notifyMailInsertMonthlyFixBilling(ctx, r.MailClient, recs)
if err != nil {
// send error
r.Logger.Error("notify mail send error", zap.Error(err))
return err
}
r.Logger.Info("send notify mail", zap.String("mail_address", os.Getenv("MAIL_TO")))
} else {
r.Logger.Info("MAIL_TO is not set. sending a notify mail skipped.")
}
return nil
}
func notifyMailInsertMonthlyFixBilling(ctx context.Context, MailClient MailClient, recs []openapi.Record) (err error) {
to := os.Getenv("MAIL_TO")
title := "[Mawinter] 月次固定費の登録完了"
body := model.NewMailMonthlyFixBilling(recs)
return MailClient.Send(ctx, to, title, body)
}
package repository
import (
"context"
"errors"
"fmt"
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"mawinter-server/internal/timeutil"
"gorm.io/gorm"
)
const CategoryTableName = "Category"
const RecordTableName = "Record"
type DBRepository struct {
Conn *gorm.DB
}
func (d *DBRepository) CloseDB() (err error) {
dbconn, err := d.Conn.DB()
if err != nil {
return err
}
return dbconn.Close()
}
func (d *DBRepository) InsertRecord(req openapi.ReqRecord) (rec openapi.Record, err error) {
dbRec, err := NewDBModelRecord(req)
if err != nil {
return openapi.Record{}, err
}
dbRes := d.Conn.Table(RecordTableName).Create(&dbRec)
if dbRes.Error != nil {
return openapi.Record{}, dbRes.Error
}
rec, err = NewRecordFromDB(dbRec)
if err != nil {
return openapi.Record{}, err
}
return rec, nil
}
func (d *DBRepository) GetRecords(ctx context.Context, num int, offset int) (recs []openapi.Record, err error) {
res := d.Conn.Table(RecordTableName).Order("id DESC").Limit(num).Offset(offset).Find(&recs)
if res.Error != nil {
return []openapi.Record{}, res.Error
}
return recs, nil
}
func (d *DBRepository) GetRecordsCount(ctx context.Context) (num int, err error) {
res := d.Conn.Table(RecordTableName).Raw("SELECT count(1) FROM Record").Scan(&num)
if res.Error != nil {
return 0, res.Error
}
return num, nil
}
func (d *DBRepository) GetCategories(ctx context.Context) (cats []model.Category, err error) {
err = d.Conn.Table(CategoryTableName).Find(&cats).Error
return cats, err
}
func (d *DBRepository) GetMonthRecords(yyyymm string) (recs []openapi.Record, err error) {
var res *gorm.DB
startDate, err := yyyymmToInitDayTime(yyyymm)
if err != nil {
return []openapi.Record{}, err
}
endDate := startDate.AddDate(0, 1, 0)
res = d.Conn.Debug().Table(RecordTableName).
Where("datetime >= ? AND datetime < ?", startDate, endDate).
Find(&recs)
if errors.Is(res.Error, gorm.ErrRecordNotFound) { // TODO: 正しくは Error 1146 をハンドリングする
return []openapi.Record{}, model.ErrNotFound
} else if res.Error != nil {
return []openapi.Record{}, res.Error
}
return recs, nil
}
func (d *DBRepository) GetMonthRecordsRecent(yyyymm string, num int) (recs []openapi.Record, err error) {
startDate, err := yyyymmToInitDayTime(yyyymm)
if err != nil {
return []openapi.Record{}, err
}
endDate := startDate.AddDate(0, 1, 0)
res := d.Conn.Table(RecordTableName).Where("datetime >= ? AND datetime < ?", startDate, endDate).Order("id DESC").Limit(num).Find(&recs)
if errors.Is(res.Error, gorm.ErrRecordNotFound) { // TODO: 正しくは Error 1146 をハンドリングする
return []openapi.Record{}, model.ErrNotFound
} else if res.Error != nil {
return []openapi.Record{}, res.Error
}
return recs, nil
}
func (d *DBRepository) MakeCategoryNameMap() (cnf map[int]string, err error) {
cnf = make(map[int]string)
var catTable []model.Category
res := d.Conn.Table("Category").Find(&catTable)
if res.Error != nil {
return nil, res.Error
}
for _, c := range catTable {
cnf[int(c.CategoryID)] = c.Name
}
return cnf, nil
}
// SumPriceForEachCatID は月間サマリ中間構造体を取得する(category_id 昇順)。
func (d *DBRepository) GetMonthMidSummary(yyyymm string) (summon []model.CategoryMidMonthSummary, err error) {
startDate, err := yyyymmToInitDayTime(yyyymm)
if err != nil {
return []model.CategoryMidMonthSummary{}, err
}
endDate := startDate.AddDate(0, 1, 0)
sqlWhere := fmt.Sprintf("datetime >= \"%s\" AND datetime < \"%s\"", startDate.Format("20060102"), endDate.Format("20060102"))
sql := fmt.Sprintf(`SELECT category_id, count(1), sum(price) FROM Record WHERE %s GROUP BY category_id ORDER BY category_id`, sqlWhere)
rows, err := d.Conn.Raw(sql).Rows()
if err != nil {
return []model.CategoryMidMonthSummary{}, err
}
defer rows.Close()
for rows.Next() {
var cm model.CategoryMidMonthSummary
err = rows.Scan(&cm.CategoryId, &cm.Count, &cm.Price)
if err != nil {
return []model.CategoryMidMonthSummary{}, err
}
summon = append(summon, cm)
}
return summon, nil
}
// InsertMonthlyFixBilling は Record に固定費を登録する
func (d *DBRepository) InsertMonthlyFixBilling(yyyymm string) (recs []openapi.Record, err error) {
var mfb []model.MonthlyFixBillingDB // DBのモデル
var fixBills []model.MonthlyFixBilling // 中間構造体
var records []model.Record // レコードDB追加用の構造体
err = d.Conn.Transaction(func(tx *gorm.DB) error {
nerr := tx.Table("Monthly_Fix_Billing").Find(&fixBills).Error // 固定費テーブルからデータ取得
if nerr != nil {
return nerr
}
for _, v := range mfb {
fixBills = append(fixBills,
model.MonthlyFixBilling{
CategoryID: int(v.CategoryID),
Day: int(v.Day),
Price: int(v.Price),
Type: v.Type,
Memo: v.Memo,
},
)
}
for _, v := range fixBills {
addrec, nerr := v.ConvAddDBModel(yyyymm)
if err != nil {
return nerr
}
records = append(records, addrec)
}
doneRec := model.MonthlyFixDoneDB{
YYYYMM: yyyymm,
Done: 1,
}
nerr = tx.Create(&doneRec).Error // 月固定データ追加記録
if nerr != nil {
return nerr
}
if len(records) > 0 {
// 挿入すべきデータがある場合: issse #62
nerr = tx.Table(RecordTableName).Create(&records).Error // 月固定データ追加
if nerr != nil {
return nerr
}
}
// commit
return nil
})
if err != nil {
return []openapi.Record{}, err
}
// API返却用に構造体を変換
cnf, err := d.MakeCategoryNameMap()
if err != nil {
return []openapi.Record{}, err
}
for _, v := range records {
rec, err := NewRecordFromDB(v)
if err != nil {
return []openapi.Record{}, err
}
rec.CategoryName = cnf[rec.CategoryId]
recs = append(recs, rec)
}
return recs, nil
}
// GetMonthlyFixDone は 固定費が登録済かどうかを取得する
// done = false なら未登録
func (d *DBRepository) GetMonthlyFixDone(yyyymm string) (done bool, err error) {
var mfd model.MonthlyFixDoneDB
err = d.Conn.Where("yyyymm = ?", yyyymm).Take(&mfd).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
} else {
return false, err
}
}
// already registed
return true, nil
}
func dbModelToConfirmInfo(mc model.MonthlyConfirm) (yc openapi.ConfirmInfo) {
var statusBool bool
if mc.Confirm == uint8(1) {
statusBool = true
} else {
statusBool = false
}
yc = openapi.ConfirmInfo{
Status: &statusBool,
Yyyymm: &mc.YYYYMM,
}
// status = false の場合は、ConfirmDatetime はDBにあっても無視する
if statusBool {
yc.ConfirmDatetime = &mc.ConfirmDatetime
}
return yc
}
func (d *DBRepository) GetMonthlyConfirm(yyyymm string) (yc openapi.ConfirmInfo, err error) {
var mc model.MonthlyConfirm
boolFalse := false
err = d.Conn.Debug().Table("Monthly_Confirm").Where("yyyymm = ?", yyyymm).Take(&mc).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return openapi.ConfirmInfo{
ConfirmDatetime: nil,
Status: &boolFalse,
Yyyymm: &yyyymm,
}, nil
} else {
return openapi.ConfirmInfo{}, err
}
}
yc = dbModelToConfirmInfo(mc)
return yc, nil
}
func (d *DBRepository) UpdateMonthlyConfirm(yyyymm string, confirm bool) (yc openapi.ConfirmInfo, err error) {
var mc model.MonthlyConfirm
var confirmNum uint8
t := timeutil.NowFunc()
// confirm
if confirm {
confirmNum = uint8(1)
} else {
confirmNum = uint8(0)
}
err = d.Conn.Transaction(func(tx *gorm.DB) error {
// GET
nerr := tx.Debug().Table("Monthly_Confirm").Where("yyyymm = ?", yyyymm).Take(&mc).Error
if nerr != nil && !errors.Is(nerr, gorm.ErrRecordNotFound) {
return nerr
}
// UPSERT
mc = model.MonthlyConfirm{
YYYYMM: yyyymm,
Confirm: confirmNum,
ConfirmDatetime: t,
}
nerr = tx.Debug().Table("Monthly_Confirm").Save(&mc).Error
if nerr != nil {
return nerr
}
// commit
return nil
})
if err != nil {
return openapi.ConfirmInfo{}, err
}
yc = dbModelToConfirmInfo(mc)
return yc, nil
}
package repository
import (
"errors"
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"mawinter-server/internal/register"
"gorm.io/gorm"
)
// InsertUniqueCatIDRecord は 同一のカテゴリIDがない場合ときに挿入、既にあればエラーを返す
func (d *DBRepository) InsertUniqueCatIDRecord(req openapi.Record) (res openapi.Record, err error) {
yyyymm := req.Datetime.Format("200601")
startDate, err := yyyymmToInitDayTime(yyyymm)
if err != nil {
return openapi.Record{}, err
}
endDate := startDate.AddDate(0, 1, 0)
err = d.Conn.Table(RecordTableName).
Where("category_id = ?", req.CategoryId).
Where("datetime >= ? AND datetime < ?", startDate, endDate).
Take(&model.Record{}).Error
if err == nil {
// already recorded
return openapi.Record{}, register.ErrAlreadyRegisted
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
// unknown error
return openapi.Record{}, err
}
dbres := d.Conn.Table(RecordTableName).Create(&req)
if dbres.Error != nil {
return openapi.Record{}, dbres.Error
}
res = req
return res, nil
}
func (d *DBRepository) GetMonthlyFixBilling() (fixBills []model.MonthlyFixBilling, err error) {
var recs []model.MonthlyFixBillingDB
err = d.Conn.Find(&recs).Error
if err != nil {
return []model.MonthlyFixBilling{}, err
}
for _, v := range recs {
fixBills = append(fixBills,
model.MonthlyFixBilling{
CategoryID: int(v.CategoryID),
Day: int(v.Day),
Price: int(v.Price),
Type: v.Type,
Memo: v.Memo,
},
)
}
return fixBills, nil
}
package repository
import (
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"mawinter-server/internal/timeutil"
"time"
)
var jst *time.Location
func init() {
j, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
jst = j
}
func yyyymmToInitDayTime(yyyymm string) (t time.Time, err error) {
// yyyymm -> time.Time
// yyyymm から その月の1日目の time.Time を返す
t, err = time.ParseInLocation("200601", yyyymm, jst)
if err != nil {
return time.Time{}, err
}
return t, nil
}
func NewDBModelRecord(req openapi.ReqRecord) (rec model.Record, err error) {
// ID is not set
rec.CategoryID = int64(req.CategoryId)
if req.Datetime != nil {
// YYYYMMDD
rec.Datetime, err = time.ParseInLocation("20060102", *req.Datetime, jst)
if err != nil {
return model.Record{}, nil
}
} else {
// default
rec.Datetime = timeutil.NowFunc()
}
rec.Price = int64(req.Price)
if req.From != nil {
rec.From = *req.From
} else {
rec.From = ""
}
if req.Type != nil {
rec.Type = *req.Type
} else {
rec.Type = ""
}
if req.Memo != nil {
rec.Memo = *req.Memo
} else {
rec.Memo = ""
}
// CreatedAt time.Time `gorm:"column:created_at"`
// UpdatedAt time.Time `gorm:"column:updated_at"`
return rec, nil
}
// NewRecordFromDB では Record テーブルをもとに、API Structを出力する。
func NewRecordFromDB(req model.Record) (rec openapi.Record, err error) {
rec = openapi.Record{
CategoryId: int(req.CategoryID),
// CategoryName: req.CategoryName : ここでは取得しない
Datetime: req.Datetime,
From: req.From,
Id: int(req.ID),
Memo: req.Memo,
Price: int(req.Price),
Type: req.Type,
}
return rec, nil
}
package server
import (
"context"
"encoding/json"
"errors"
"fmt"
"mawinter-server/internal/model"
"mawinter-server/internal/openapi"
"mawinter-server/internal/timeutil"
"net/http"
"strconv"
"go.uber.org/zap"
)
type apigateway struct {
Logger *zap.Logger
ap2 APIServiceV2
}
func (a *apigateway) Get(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "It is the root page.\n")
}
// V2
// (POST /v2/record)
func (a *apigateway) PostV2Record(w http.ResponseWriter, r *http.Request) {
var req openapi.ReqRecord
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
return
}
ctx := context.Background()
rec, err := a.ap2.PostRecord(ctx, req)
if err != nil && errors.Is(err, model.ErrUnknownCategoryID) {
// Category ID情報がDBにない場合
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
return
} else if err != nil && errors.Is(err, model.ErrAlreadyRecorded) {
// confirm month 確定済の月だった場合
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "already confirmed month")
return
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&rec)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, string(outputJson))
}
// (GET /v2/record/count)
func (a *apigateway) GetV2RecordCount(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
rec, err := a.ap2.GetRecordsCount(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&rec)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
// (POST /v2/record/fixmonth)
func (a *apigateway) PostV2RecordFixmonth(w http.ResponseWriter, r *http.Request, params openapi.PostV2RecordFixmonthParams) {
ctx := context.Background()
var yms string
if params.Yyyymm == nil {
// default value
yms = timeutil.NowFunc().Format("200601")
} else {
yms = strconv.Itoa(*params.Yyyymm)
}
recs, err := a.ap2.PostMonthlyFixRecord(ctx, yms)
if errors.Is(err, model.ErrAlreadyRecorded) {
w.WriteHeader(http.StatusNoContent)
return
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
outputJson, err := json.Marshal(&recs)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, string(outputJson))
}
// (GET /v2/record)
func (a *apigateway) GetV2Record(w http.ResponseWriter, r *http.Request, params openapi.GetV2RecordParams) {
ctx := context.Background()
num := 20 // default value
if params.Num != nil {
num = *params.Num
}
offset := 0 // default value
if params.Offset != nil {
offset = *params.Offset
}
recs, err := a.ap2.GetRecords(ctx, num, offset)
if errors.Is(err, model.ErrNotFound) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, err.Error())
return
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&recs)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
// Your GET endpoint
// (GET /v2/record/summary/{year})
func (a *apigateway) GetV2RecordYear(w http.ResponseWriter, r *http.Request, year int) {
ctx := context.Background()
yearSummary, err := a.ap2.GetV2YearSummary(ctx, year)
if err != nil {
if errors.Is(err, model.ErrInvalidValue) {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
return
}
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&yearSummary)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, string(outputJson))
}
// Your GET endpoint
// (GET /v2/record/{yyyymm})
func (a *apigateway) GetV2RecordYyyymm(w http.ResponseWriter, r *http.Request, yyyymm string, params openapi.GetV2RecordYyyymmParams) {
ctx := context.Background()
err := model.ValidYYYYMM(yyyymm)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
return
}
recs, err := a.ap2.GetYYYYMMRecords(ctx, yyyymm, params)
if errors.Is(err, model.ErrNotFound) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, err.Error())
return
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&recs)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
// GET v2/record/{yyyymm}/recent?num=x
func (a *apigateway) GetV2RecordYyyymmRecent(w http.ResponseWriter, r *http.Request, yyyymm string, params openapi.GetV2RecordYyyymmRecentParams) {
const defaultNum = 10
ctx := context.Background()
if params.Num == nil {
params.Num = int2ptr(defaultNum)
}
err := model.ValidYYYYMM(yyyymm)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
return
}
recs, err := a.ap2.GetYYYYMMRecordsRecent(ctx, yyyymm, *params.Num)
if errors.Is(err, model.ErrNotFound) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, err.Error())
return
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&recs)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
// (GET /v2/record/{yyyymm}/confirm)
func (a *apigateway) GetV2RecordYyyymmConfirm(w http.ResponseWriter, r *http.Request, yyyymm string) {
ctx := context.Background()
err := model.ValidYYYYMM(yyyymm)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
}
yc, err := a.ap2.GetMonthlyConfirm(ctx, yyyymm)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&yc)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
// (PUT /v2/record/{yyyymm}/confirm)
func (a *apigateway) PutV2TableYyyymmConfirm(w http.ResponseWriter, r *http.Request, yyyymm string) {
ctx := context.Background()
err := model.ValidYYYYMM(yyyymm)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
}
var req openapi.ConfirmInfo
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
return
}
yc, err := a.ap2.UpdateMonthlyConfirm(ctx, yyyymm, *req.Status)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&yc)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
// (GET /version)
func (a *apigateway) GetVersion(w http.ResponseWriter, r *http.Request) {
vers := openapi.GetVersionJSONBody{
Version: str2ptr(Version),
Revision: str2ptr(Revision),
Build: str2ptr(Build),
}
outputJson, err := json.Marshal(&vers)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
func (a *apigateway) GetCategories(w http.ResponseWriter, r *http.Request) {
cats, err := a.ap2.GetCategories(r.Context())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
outputJson, err := json.Marshal(&cats)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(outputJson))
}
func str2ptr(a string) *string {
return &a
}
func int2ptr(a int) *int {
return &a
}
package server
import (
"context"
"mawinter-server/internal/openapi"
"net/http"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
// Binary Info
var (
Version string
Revision string
Build string
)
// V2
type APIServiceV2 interface {
// V2
PostRecord(ctx context.Context, req openapi.ReqRecord) (rec openapi.Record, err error)
PostMonthlyFixRecord(ctx context.Context, yyyymm string) (recs []openapi.Record, err error)
GetRecords(ctx context.Context, num int, offset int) (recs []openapi.Record, err error)
GetRecordsCount(ctx context.Context) (rec openapi.RecordCount, err error)
GetCategories(ctx context.Context) (recs []openapi.Category, err error)
GetYYYYMMRecords(ctx context.Context, yyyymm string, params openapi.GetV2RecordYyyymmParams) (recs []openapi.Record, err error)
GetYYYYMMRecordsRecent(ctx context.Context, yyyymm string, num int) (recs []openapi.Record, err error)
GetV2YearSummary(ctx context.Context, year int) (sums []openapi.CategoryYearSummary, err error)
GetMonthlyConfirm(ctx context.Context, yyyymm string) (yc openapi.ConfirmInfo, err error)
UpdateMonthlyConfirm(ctx context.Context, yyyymm string, confirm bool) (yc openapi.ConfirmInfo, err error)
}
type Server struct {
Logger *zap.Logger
Ap2 APIServiceV2
BasicAuth struct {
User string
Pass string
}
}
func (s *Server) Start(ctx context.Context) error {
swagger, err := openapi.GetSwagger()
if err != nil {
s.Logger.Error("failed to get swagger spec", zap.Error(err))
return err
}
swagger.Servers = nil
r := chi.NewRouter()
r.Use(s.middlewareLogging)
openapi.HandlerFromMux(&apigateway{Logger: s.Logger, ap2: s.Ap2}, r)
addr := ":8080"
if err := http.ListenAndServe(addr, r); err != nil {
s.Logger.Error("failed to listen and serve", zap.Error(err))
return err
}
return nil
}
func (s *Server) middlewareLogging(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
s.Logger.Info("access", zap.String("url", r.URL.Path), zap.String("X-Forwarded-For", r.Header.Get("X-Forwarded-For")))
}
h.ServeHTTP(w, r)
})
}
package service
import (
"context"
"fmt"
"mawinter-server/internal/openapi"
"os"
"strings"
"time"
"go.uber.org/zap"
)
var jst *time.Location
func init() {
j, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
jst = j
}
type APIServiceDup interface {
GetYYYYMMRecords(ctx context.Context, yyyymm string, params openapi.GetV2RecordYyyymmParams) (recs []openapi.Record, err error)
}
type MailClient interface {
Send(ctx context.Context, to string, title string, body string) (err error)
}
type DuplicateCheckService struct {
Logger *zap.Logger
Ap APIServiceDup
MailClient MailClient
}
func judgeDuplicateRecords(d1 openapi.Record, d2 openapi.Record) bool {
// CategoryID と Price と Datetime の日付までが一致していれば True(重複可能性あり)とする
if d1.CategoryId != d2.CategoryId {
return false
}
if d1.Price != d2.Price {
return false
}
if d1.Datetime.Format("20060102") != d2.Datetime.Format("20060102") {
return false
}
return true
}
func (d *DuplicateCheckService) DuplicateCheck(ctx context.Context, yyyymm string) (err error) {
d.Logger.Info("DuplicateCheck start")
var dupInt = 0
recs, err := d.Ap.GetYYYYMMRecords(ctx, yyyymm, openapi.GetV2RecordYyyymmParams{})
if err != nil {
return err
}
var targets []openapi.Record // 重複判定する対象
for _, r := range recs {
if strings.Contains(r.Type, "D") {
// Type に 'D' が入っているレコードは重複判定の対象外
continue
}
targets = append(targets, r)
}
d.Logger.Info("fetch all records from monthly table")
mailbody := "duplicate data: \n"
// targets 内全体に重複の判定をかける
for i, u := range targets {
for j, v := range targets {
// ダブルカウントを防ぐため、i < j とする
if i >= j {
continue
}
if judgeDuplicateRecords(u, v) {
mailbody += fmt.Sprintf("date = %s, category_id = %d, price = %d\n", u.Datetime, u.CategoryId, u.Price)
d.Logger.Info("detect duplicate data", zap.Time("Date", u.Datetime))
dupInt++
}
}
}
d.Logger.Info("DuplicateCheck complete", zap.Int("rec_num", len(recs)), zap.Int("target_num", len(targets)), zap.Int("duplicate_num", dupInt))
if dupInt > 0 {
err = d.MailClient.Send(ctx, os.Getenv("MAIL_TO"), "[mawinter] detect duplicate report", mailbody)
if err != nil {
d.Logger.Error("detect duplicate send mail error", zap.Error(err))
return err
}
}
return nil
}
package service
import (
"context"
"mawinter-server/internal/openapi"
"time"
)
type mockAp struct{}
type mockMailClient struct{}
func (m *mockAp) GetYYYYMMRecords(ctx context.Context, yyyymm string, params openapi.GetV2RecordYyyymmParams) (recs []openapi.Record, err error) {
return []openapi.Record{
{
CategoryId: 100,
Datetime: time.Date(2000, 1, 23, 12, 0, 0, 0, jst),
From: "from1",
Price: 1234,
},
{
CategoryId: 100,
Datetime: time.Date(2000, 1, 23, 0, 0, 0, 0, jst),
From: "from1",
Price: 1234,
},
{
CategoryId: 200,
Datetime: time.Date(2000, 1, 23, 0, 0, 0, 0, jst),
From: "from1",
Price: 1234,
},
}, nil
}
func (m *mockMailClient) Send(ctx context.Context, to string, title string, body string) (err error) {
return nil
}