Compare commits
6 Commits
0258ff6620
...
main
Author | SHA1 | Date | |
---|---|---|---|
dd999b9355 | |||
14ee642aab | |||
b30a5c5c2d | |||
58cff774e6 | |||
716a58d44c | |||
de7c6f7223 |
18
README.md
18
README.md
@ -564,3 +564,21 @@ querier.
|
||||
Since this repo layer is just a wrapping layer between the `sqlc.models` and
|
||||
my own models, I can extract the conversion part to functions and test them.
|
||||
I'm not testing the whole thing but I test what I can.
|
||||
|
||||
### 2024/10/24
|
||||
|
||||
When writing the tests. I am asking myself the differences between `[]T`,
|
||||
`[]*T` and `*[]T`.
|
||||
|
||||
`*[]T` is simple, it is a reference to the original slice. So modifying it
|
||||
means modifying the original slice.
|
||||
|
||||
But between `[]*T` and `[]T`, the only difference that I see (pointed out by
|
||||
`ChatGPT`) is how the memory is allocated. With `[]T` it might be better for
|
||||
the GC to deal with the memory free. I thing for my project I will stick to
|
||||
`[]T`.
|
||||
|
||||
### 2024/10/25
|
||||
|
||||
Read this [article](https://konradreiche.com/blog/two-common-go-interface-misuses/)
|
||||
today, maybe I am abusing the usage of interfaces?
|
||||
|
@ -25,7 +25,9 @@ package repo
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
||||
)
|
||||
@ -34,6 +36,8 @@ type dbRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const queryTimeout = 3 * time.Second
|
||||
|
||||
func NewDBRepository(db *sql.DB) repo.DBRepository {
|
||||
return &dbRepository{
|
||||
db: db,
|
||||
@ -66,3 +70,11 @@ func (dr *dbRepository) Transaction(
|
||||
data, err := txFunc(ctx, tx)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func getQueries(queries *sqlc.Queries, tx any) *sqlc.Queries {
|
||||
transaction, ok := tx.(*sql.Tx)
|
||||
if ok {
|
||||
return sqlc.New(transaction)
|
||||
}
|
||||
return queries
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||
@ -35,7 +36,7 @@ import (
|
||||
)
|
||||
|
||||
type eventRepository struct {
|
||||
queries sqlc.Querier
|
||||
queries *sqlc.Queries
|
||||
}
|
||||
|
||||
func NewEventRepository(db *sql.DB) repo.EventRepository {
|
||||
@ -48,8 +49,35 @@ func NewEventRepository(db *sql.DB) repo.EventRepository {
|
||||
func (e *eventRepository) Create(
|
||||
ctx context.Context,
|
||||
evEntity *model.EventEntity,
|
||||
tx any,
|
||||
) (*model.EventEntity, error) {
|
||||
panic("unimplemented")
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
event, err := queries.InsertEvent(timeoutCtx, sqlc.InsertEventParams{
|
||||
Name: evEntity.Name,
|
||||
Description: sql.NullString{String: evEntity.Description, Valid: true},
|
||||
TotalAmount: sql.NullInt32{Int32: int32(evEntity.TotalAmount), Valid: true},
|
||||
DefaultCurrency: evEntity.DefaultCurrency,
|
||||
OwnerID: int32(evEntity.OwnerID),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.EventEntity{
|
||||
ID: int(event.ID),
|
||||
Name: event.Name,
|
||||
Description: event.Description.String,
|
||||
TotalAmount: int(event.TotalAmount.Int32),
|
||||
DefaultCurrency: event.DefaultCurrency,
|
||||
OwnerID: int(event.OwnerID),
|
||||
CreatedAt: event.CreatedAt,
|
||||
UpdatedAt: event.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved, error) {
|
||||
@ -62,7 +90,7 @@ func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []*model.UserBaseRetrieved
|
||||
var users []model.UserBaseRetrieved
|
||||
err = json.Unmarshal(eventDTO.Users, &users)
|
||||
if err != nil {
|
||||
// Unexpected
|
||||
@ -89,8 +117,17 @@ func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved
|
||||
}
|
||||
|
||||
// GetByID implements repo.EventRepository.
|
||||
func (e *eventRepository) GetByID(ctx context.Context, eventID int) (*model.EventRetrieved, error) {
|
||||
eventDTO, err := e.queries.GetEventByID(ctx, int32(eventID))
|
||||
func (e *eventRepository) GetByID(
|
||||
ctx context.Context,
|
||||
eventID int,
|
||||
tx any,
|
||||
) (*model.EventRetrieved, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
eventDTO, err := queries.GetEventByID(timeoutCtx, int32(eventID))
|
||||
if err != nil {
|
||||
log.ErrorLog("query error", "err", err)
|
||||
return nil, err
|
||||
@ -99,10 +136,10 @@ func (e *eventRepository) GetByID(ctx context.Context, eventID int) (*model.Even
|
||||
return convToEventRetrieved(&eventDTO)
|
||||
}
|
||||
|
||||
func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]*model.EventListRetrieved, error) {
|
||||
var events []*model.EventListRetrieved
|
||||
func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]model.EventListRetrieved, error) {
|
||||
events := make([]model.EventListRetrieved, len(eventsDTO))
|
||||
|
||||
for _, evDTO := range eventsDTO {
|
||||
for i, evDTO := range eventsDTO {
|
||||
var owner model.UserBaseRetrieved
|
||||
err := json.Unmarshal(evDTO.Owner, &owner)
|
||||
if err != nil {
|
||||
@ -110,13 +147,15 @@ func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]*model.EventList
|
||||
log.ErrorLog("json unmarshal error", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
ev := &model.EventListRetrieved{
|
||||
ev := model.EventListRetrieved{
|
||||
ID: int(evDTO.ID),
|
||||
Name: evDTO.Name,
|
||||
Description: evDTO.Description.String,
|
||||
Owner: &owner,
|
||||
CreatedAt: evDTO.CreatedAt,
|
||||
}
|
||||
|
||||
events = append(events, ev)
|
||||
events[i] = ev
|
||||
}
|
||||
|
||||
return events, nil
|
||||
@ -126,8 +165,14 @@ func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]*model.EventList
|
||||
func (e *eventRepository) ListEventsByUserID(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
) ([]*model.EventListRetrieved, error) {
|
||||
eventsDTO, err := e.queries.ListEventsByUserID(ctx, int32(userID))
|
||||
tx any,
|
||||
) ([]model.EventListRetrieved, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
eventsDTO, err := queries.ListEventsByUserID(timeoutCtx, int32(userID))
|
||||
if err != nil {
|
||||
log.ErrorLog("query error", "err", err)
|
||||
return nil, err
|
||||
@ -140,8 +185,14 @@ func (e *eventRepository) ListEventsByUserID(
|
||||
func (e *eventRepository) UpdateEventByID(
|
||||
ctx context.Context,
|
||||
event *model.EventUpdateEntity,
|
||||
tx any,
|
||||
) error {
|
||||
err := e.queries.UpdateEventByID(ctx, sqlc.UpdateEventByIDParams{
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
err := queries.UpdateEventByID(timeoutCtx, sqlc.UpdateEventByIDParams{
|
||||
ID: int32(event.ID),
|
||||
Name: event.Name,
|
||||
Description: sql.NullString{String: event.Description, Valid: true},
|
||||
@ -149,3 +200,77 @@ func (e *eventRepository) UpdateEventByID(
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetParticipation implements repo.EventRepository.
|
||||
func (e *eventRepository) GetParticipation(
|
||||
ctx context.Context,
|
||||
userID, eventID int,
|
||||
tx any,
|
||||
) (*model.ParticipationEntity, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
partDTO, err := queries.GetParticipation(timeoutCtx, sqlc.GetParticipationParams{
|
||||
UserID: int32(userID),
|
||||
EventID: int32(eventID),
|
||||
})
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// No error, but participation not found
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.ParticipationEntity{
|
||||
ID: int(partDTO.ID),
|
||||
UserID: int(partDTO.UserID),
|
||||
EventID: int(partDTO.EventID),
|
||||
InvitedByUserID: int(partDTO.InvitedByUserID.Int32),
|
||||
CreatedAt: partDTO.CreatedAt,
|
||||
UpdatedAt: partDTO.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InsertParticipation implements repo.EventRepository.
|
||||
func (e *eventRepository) InsertParticipation(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
eventID int,
|
||||
invitedByUserID int,
|
||||
tx any,
|
||||
) (*model.ParticipationEntity, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
var invitedBy sql.NullInt32
|
||||
if invitedByUserID == 0 {
|
||||
invitedBy = sql.NullInt32{Int32: 0, Valid: false}
|
||||
} else {
|
||||
invitedBy = sql.NullInt32{Int32: int32(invitedByUserID), Valid: true}
|
||||
}
|
||||
participationDTO, err := queries.InsertParticipation(timeoutCtx, sqlc.InsertParticipationParams{
|
||||
UserID: int32(userID),
|
||||
EventID: int32(eventID),
|
||||
InvitedByUserID: invitedBy,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.ParticipationEntity{
|
||||
ID: int(participationDTO.ID),
|
||||
UserID: int(participationDTO.UserID),
|
||||
EventID: int(participationDTO.EventID),
|
||||
InvitedByUserID: int(participationDTO.InvitedByUserID.Int32),
|
||||
CreatedAt: participationDTO.CreatedAt,
|
||||
UpdatedAt: participationDTO.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
122
internal/howmuch/adapter/repo/event_test.go
Normal file
122
internal/howmuch/adapter/repo/event_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvToEventRetrieved(t *testing.T) {
|
||||
input := &sqlc.GetEventByIDRow{
|
||||
ID: 123,
|
||||
Name: "event",
|
||||
Description: sql.NullString{Valid: false},
|
||||
TotalAmount: sql.NullInt32{Valid: false},
|
||||
DefaultCurrency: "EUR",
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
||||
Users: json.RawMessage(`[{"id":1, "first_name":"owner", "last_name":"owner"}]`),
|
||||
}
|
||||
|
||||
want := &model.EventRetrieved{
|
||||
ID: 123,
|
||||
Name: "event",
|
||||
Description: "",
|
||||
TotalAmount: model.Money{Amount: 0, Currency: "EUR"},
|
||||
DefaultCurrency: model.Currency("EUR"),
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Owner: &model.UserBaseRetrieved{
|
||||
ID: 1,
|
||||
FirstName: "owner",
|
||||
LastName: "owner",
|
||||
},
|
||||
Users: []model.UserBaseRetrieved{
|
||||
{
|
||||
ID: 1,
|
||||
FirstName: "owner",
|
||||
LastName: "owner",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := convToEventRetrieved(input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestConvToEventList(t *testing.T) {
|
||||
input := []sqlc.ListEventsByUserIDRow{
|
||||
{
|
||||
ID: 123,
|
||||
Name: "event",
|
||||
Description: sql.NullString{Valid: false},
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
||||
},
|
||||
{
|
||||
ID: 456,
|
||||
Name: "event2",
|
||||
Description: sql.NullString{String: "super event", Valid: true},
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
||||
},
|
||||
}
|
||||
|
||||
want := []model.EventListRetrieved{
|
||||
{
|
||||
ID: 123,
|
||||
Name: "event",
|
||||
Description: "",
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Owner: &model.UserBaseRetrieved{
|
||||
ID: 1,
|
||||
FirstName: "owner",
|
||||
LastName: "owner",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 456,
|
||||
Name: "event2",
|
||||
Description: "super event",
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Owner: &model.UserBaseRetrieved{
|
||||
ID: 1,
|
||||
FirstName: "owner",
|
||||
LastName: "owner",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := convToEventList(input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
267
internal/howmuch/adapter/repo/expense.go
Normal file
267
internal/howmuch/adapter/repo/expense.go
Normal file
@ -0,0 +1,267 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
||||
)
|
||||
|
||||
type expenseRepository struct {
|
||||
queries *sqlc.Queries
|
||||
}
|
||||
|
||||
func NewExpenseRepository(db *sql.DB) repo.ExpenseRepository {
|
||||
return &expenseRepository{
|
||||
queries: sqlc.New(db),
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteExpense implements repo.ExpenseRepository.
|
||||
func (e *expenseRepository) DeleteExpense(ctx context.Context, expenseID int, tx any) error {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
return queries.DeleteExpense(timeoutCtx, int32(expenseID))
|
||||
}
|
||||
|
||||
// DeleteTransactionsOfExpense implements repo.ExpenseRepository.
|
||||
func (e *expenseRepository) DeleteTransactionsOfExpense(
|
||||
ctx context.Context,
|
||||
expenseID int,
|
||||
tx any,
|
||||
) error {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
return queries.DeleteTransactionsOfExpenseID(timeoutCtx, int32(expenseID))
|
||||
}
|
||||
|
||||
// GetExpenseByID implements repo.ExpenseRepository.
|
||||
func (e *expenseRepository) GetExpenseByID(
|
||||
ctx context.Context,
|
||||
expenseID int,
|
||||
tx any,
|
||||
) (*model.ExpenseRetrieved, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
expenseDTO, err := queries.GetExpenseByID(timeoutCtx, int32(expenseID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expense, err := convToExpenseRetrieved(&expenseDTO)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return expense, nil
|
||||
}
|
||||
|
||||
func convToPayments(raw json.RawMessage) ([]model.Payment, error) {
|
||||
var paymentsRetrieved []model.PaymentRetrieved
|
||||
err := json.Unmarshal(raw, &paymentsRetrieved)
|
||||
if err != nil {
|
||||
// Unexpected
|
||||
log.ErrorLog("json unmarshal error", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payments := make([]model.Payment, len(paymentsRetrieved))
|
||||
for i, p := range paymentsRetrieved {
|
||||
payment := model.Payment{
|
||||
PayerID: p.PayerID,
|
||||
PayerFirstName: p.PayerFirstName,
|
||||
PayerLastName: p.PayerLastName,
|
||||
Amount: model.MakeMoney(p.Amount, model.Currency(p.Currency)),
|
||||
}
|
||||
payments[i] = payment
|
||||
}
|
||||
|
||||
return payments, nil
|
||||
}
|
||||
|
||||
func convToBenefits(raw json.RawMessage) ([]model.Benefit, error) {
|
||||
var benefitsRetrieved []model.BenefitRetrieved
|
||||
err := json.Unmarshal(raw, &benefitsRetrieved)
|
||||
if err != nil {
|
||||
// Unexpected
|
||||
log.ErrorLog("json unmarshal error", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
benefits := make([]model.Benefit, len(benefitsRetrieved))
|
||||
for i, b := range benefitsRetrieved {
|
||||
benefit := model.Benefit{
|
||||
RecipientID: b.RecipientID,
|
||||
RecipientFirstName: b.RecipientFirstName,
|
||||
RecipientLastName: b.RecipientLastName,
|
||||
Amount: model.MakeMoney(b.Amount, model.Currency(b.Currency)),
|
||||
}
|
||||
benefits[i] = benefit
|
||||
}
|
||||
|
||||
return benefits, nil
|
||||
}
|
||||
|
||||
func convToExpenseRetrieved(expenseDTO *sqlc.GetExpenseByIDRow) (*model.ExpenseRetrieved, error) {
|
||||
payments, err := convToPayments(expenseDTO.Payments)
|
||||
if err != nil {
|
||||
// Unexpected
|
||||
return nil, err
|
||||
}
|
||||
|
||||
benefits, err := convToBenefits(expenseDTO.Benefits)
|
||||
if err != nil {
|
||||
// Unexpected
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expenseRetrieved := &model.ExpenseRetrieved{
|
||||
ID: int(expenseDTO.ID),
|
||||
CreatedAt: expenseDTO.CreatedAt,
|
||||
UpdatedAt: expenseDTO.UpdatedAt,
|
||||
Amount: model.MakeMoney(int(expenseDTO.Amount), model.Currency(expenseDTO.Currency)),
|
||||
EventID: int(expenseDTO.EventID),
|
||||
Detail: model.ExpenseDetail{
|
||||
Name: expenseDTO.Name.String,
|
||||
Place: expenseDTO.Place.String,
|
||||
},
|
||||
Payments: payments,
|
||||
Benefits: benefits,
|
||||
}
|
||||
|
||||
return expenseRetrieved, nil
|
||||
}
|
||||
|
||||
// InsertExpense implements repo.ExpenseRepository.
|
||||
func (e *expenseRepository) InsertExpense(
|
||||
ctx context.Context,
|
||||
expenseEntity *model.ExpenseEntity,
|
||||
tx any,
|
||||
) (*model.ExpenseEntity, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
expenseDTO, err := queries.InsertExpense(timeoutCtx, sqlc.InsertExpenseParams{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Amount: int32(expenseEntity.Amount),
|
||||
Currency: expenseEntity.Currency,
|
||||
EventID: int32(expenseEntity.EventID),
|
||||
Name: sql.NullString{String: expenseEntity.Name, Valid: true},
|
||||
Place: sql.NullString{String: expenseEntity.Place, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.ExpenseEntity{
|
||||
ID: int(expenseDTO.ID),
|
||||
CreatedAt: expenseDTO.CreatedAt,
|
||||
UpdatedAt: expenseDTO.CreatedAt,
|
||||
Amount: int(expenseDTO.Amount),
|
||||
Currency: expenseDTO.Currency,
|
||||
EventID: int(expenseDTO.EventID),
|
||||
Name: expenseDTO.Name.String,
|
||||
Place: expenseDTO.Place.String,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListExpensesByEventID implements repo.ExpenseRepository.
|
||||
func (e *expenseRepository) ListExpensesByEventID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
tx any,
|
||||
) ([]model.ExpensesListRetrieved, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
listDTO, err := queries.ListExpensesByEventID(timeoutCtx, int32(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]model.ExpensesListRetrieved, len(listDTO))
|
||||
for i, dto := range listDTO {
|
||||
elem := model.ExpensesListRetrieved{
|
||||
ID: int(dto.ID),
|
||||
CreatedAt: dto.CreatedAt,
|
||||
UpdatedAt: dto.UpdatedAt,
|
||||
Amount: model.MakeMoney(int(dto.Amount), model.Currency(dto.Currency)),
|
||||
EventID: int(dto.EventID),
|
||||
Detail: model.ExpenseDetail{
|
||||
Name: dto.Name.String,
|
||||
Place: dto.Place.String,
|
||||
},
|
||||
}
|
||||
res[i] = elem
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UpdateExpenseByID implements repo.ExpenseRepository.
|
||||
func (e *expenseRepository) UpdateExpenseByID(
|
||||
ctx context.Context,
|
||||
expenseUpdate *model.ExpenseUpdateEntity,
|
||||
tx any,
|
||||
) (*model.ExpenseEntity, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(e.queries, tx)
|
||||
|
||||
expenseDTO, err := queries.UpdateExpenseByID(timeoutCtx, sqlc.UpdateExpenseByIDParams{
|
||||
ID: int32(expenseUpdate.ID),
|
||||
UpdatedAt: time.Now(),
|
||||
Amount: int32(expenseUpdate.Amount),
|
||||
Currency: expenseUpdate.Currency,
|
||||
Name: sql.NullString{String: expenseUpdate.Name, Valid: true},
|
||||
Place: sql.NullString{String: expenseUpdate.Place, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.ExpenseEntity{
|
||||
ID: int(expenseDTO.ID),
|
||||
CreatedAt: expenseDTO.CreatedAt,
|
||||
UpdatedAt: expenseDTO.CreatedAt,
|
||||
Amount: int(expenseDTO.Amount),
|
||||
Currency: expenseDTO.Currency,
|
||||
EventID: int(expenseDTO.EventID),
|
||||
Name: expenseDTO.Name.String,
|
||||
Place: expenseDTO.Place.String,
|
||||
}, nil
|
||||
}
|
96
internal/howmuch/adapter/repo/expense_test.go
Normal file
96
internal/howmuch/adapter/repo/expense_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvToExpenseRetrieved(t *testing.T) {
|
||||
input := &sqlc.GetExpenseByIDRow{
|
||||
ID: 123,
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Amount: 123,
|
||||
Currency: "EUR",
|
||||
EventID: 123,
|
||||
Name: sql.NullString{Valid: false},
|
||||
Place: sql.NullString{Valid: false},
|
||||
Payments: json.RawMessage(
|
||||
`[{"payer_id": 1, "payer_first_name": "toto", "payer_last_name": "titi", "amount": 10, "currency": "EUR"},
|
||||
{"payer_id": 2, "payer_first_name": "tata", "payer_last_name": "titi", "amount": 10, "currency": "EUR"}]`,
|
||||
),
|
||||
Benefits: json.RawMessage(
|
||||
`[{"recipient_id": 1, "recipient_first_name": "toto", "recipient_last_name": "titi", "amount": 10, "currency": "EUR"},
|
||||
{"recipient_id": 2, "recipient_first_name": "tata", "recipient_last_name": "titi", "amount": 10, "currency": "EUR"}]`,
|
||||
),
|
||||
}
|
||||
|
||||
want := &model.ExpenseRetrieved{
|
||||
ID: 123,
|
||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||
Amount: model.Money{Amount: 123, Currency: model.Currency("EUR")},
|
||||
EventID: 123,
|
||||
Detail: model.ExpenseDetail{},
|
||||
Payments: []model.Payment{
|
||||
{
|
||||
PayerID: 1,
|
||||
PayerFirstName: "toto",
|
||||
PayerLastName: "titi",
|
||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||
},
|
||||
{
|
||||
PayerID: 2,
|
||||
PayerFirstName: "tata",
|
||||
PayerLastName: "titi",
|
||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||
},
|
||||
},
|
||||
Benefits: []model.Benefit{
|
||||
{
|
||||
RecipientID: 1,
|
||||
RecipientFirstName: "toto",
|
||||
RecipientLastName: "titi",
|
||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||
},
|
||||
{
|
||||
RecipientID: 2,
|
||||
RecipientFirstName: "tata",
|
||||
RecipientLastName: "titi",
|
||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := convToExpenseRetrieved(input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
@ -24,14 +24,6 @@ FROM "expense" ex
|
||||
JOIN "event" ev ON ev.id = ex.event_id
|
||||
WHERE ev.id = $1;
|
||||
|
||||
-- name: ListExpensesByEventIDByUserID :many
|
||||
SELECT
|
||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
||||
ex.name, ex.place
|
||||
FROM "expense" ex
|
||||
JOIN "event" ev ON ev.id = ex.event_id
|
||||
WHERE ev.id = $1;
|
||||
|
||||
-- name: GetExpenseByID :one
|
||||
WITH payer_transaction as (
|
||||
SELECT pt.expense_id,
|
||||
|
@ -183,47 +183,6 @@ func (q *Queries) ListExpensesByEventID(ctx context.Context, id int32) ([]Expens
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listExpensesByEventIDByUserID = `-- name: ListExpensesByEventIDByUserID :many
|
||||
SELECT
|
||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
||||
ex.name, ex.place
|
||||
FROM "expense" ex
|
||||
JOIN "event" ev ON ev.id = ex.event_id
|
||||
WHERE ev.id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListExpensesByEventIDByUserID(ctx context.Context, id int32) ([]Expense, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listExpensesByEventIDByUserID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Expense
|
||||
for rows.Next() {
|
||||
var i Expense
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Amount,
|
||||
&i.Currency,
|
||||
&i.EventID,
|
||||
&i.Name,
|
||||
&i.Place,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateExpenseByID = `-- name: UpdateExpenseByID :one
|
||||
UPDATE "expense"
|
||||
SET updated_at = $2, amount = $3, currency = $4, name = $5, place = $6
|
||||
|
@ -3,3 +3,8 @@ INSERT INTO participation (
|
||||
user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetParticipation :one
|
||||
SELECT id, user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||
FROM "participation"
|
||||
WHERE user_id = $1 AND event_id = $2;
|
||||
|
@ -11,6 +11,31 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const getParticipation = `-- name: GetParticipation :one
|
||||
SELECT id, user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||
FROM "participation"
|
||||
WHERE user_id = $1 AND event_id = $2
|
||||
`
|
||||
|
||||
type GetParticipationParams struct {
|
||||
UserID int32
|
||||
EventID int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetParticipation(ctx context.Context, arg GetParticipationParams) (Participation, error) {
|
||||
row := q.db.QueryRowContext(ctx, getParticipation, arg.UserID, arg.EventID)
|
||||
var i Participation
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.EventID,
|
||||
&i.InvitedByUserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertParticipation = `-- name: InsertParticipation :one
|
||||
INSERT INTO participation (
|
||||
user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||
|
@ -13,6 +13,7 @@ type Querier interface {
|
||||
DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error
|
||||
GetEventByID(ctx context.Context, id int32) (GetEventByIDRow, error)
|
||||
GetExpenseByID(ctx context.Context, id int32) (GetExpenseByIDRow, error)
|
||||
GetParticipation(ctx context.Context, arg GetParticipationParams) (Participation, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (User, error)
|
||||
GetUserByID(ctx context.Context, id int32) (User, error)
|
||||
InsertEvent(ctx context.Context, arg InsertEventParams) (Event, error)
|
||||
@ -22,7 +23,6 @@ type Querier interface {
|
||||
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
|
||||
ListEventsByUserID(ctx context.Context, userID int32) ([]ListEventsByUserIDRow, error)
|
||||
ListExpensesByEventID(ctx context.Context, id int32) ([]Expense, error)
|
||||
ListExpensesByEventIDByUserID(ctx context.Context, id int32) ([]Expense, error)
|
||||
UpdateEventByID(ctx context.Context, arg UpdateEventByIDParams) error
|
||||
UpdateExpenseByID(ctx context.Context, arg UpdateExpenseByIDParams) (Expense, error)
|
||||
}
|
||||
|
@ -34,43 +34,34 @@ import (
|
||||
)
|
||||
|
||||
type userRepository struct {
|
||||
querier sqlc.Querier
|
||||
queries *sqlc.Queries
|
||||
}
|
||||
|
||||
const insertTimeout = 1 * time.Second
|
||||
|
||||
func NewUserRepository(db *sql.DB) repo.UserRepository {
|
||||
return &userRepository{
|
||||
querier: sqlc.New(db),
|
||||
queries: sqlc.New(db),
|
||||
}
|
||||
}
|
||||
|
||||
// Create
|
||||
func (ur *userRepository) Create(
|
||||
func (u *userRepository) Create(
|
||||
ctx context.Context,
|
||||
transaction interface{},
|
||||
u *model.UserEntity,
|
||||
userEntity *model.UserEntity,
|
||||
tx any,
|
||||
) (*model.UserEntity, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, insertTimeout)
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
args := sqlc.InsertUserParams{
|
||||
Email: u.Email,
|
||||
FirstName: u.FirstName,
|
||||
LastName: u.LastName,
|
||||
Password: u.Password,
|
||||
queries := getQueries(u.queries, tx)
|
||||
|
||||
userDB, err := queries.InsertUser(timeoutCtx, sqlc.InsertUserParams{
|
||||
Email: userEntity.Email,
|
||||
FirstName: userEntity.FirstName,
|
||||
LastName: userEntity.LastName,
|
||||
Password: userEntity.Password,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
tx, ok := transaction.(*sql.Tx)
|
||||
if !ok {
|
||||
return nil, errors.New("transaction is not a *sql.Tx")
|
||||
}
|
||||
|
||||
queries := sqlc.New(tx)
|
||||
|
||||
userDB, err := queries.InsertUser(timeoutCtx, args)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -87,8 +78,17 @@ func (ur *userRepository) Create(
|
||||
}
|
||||
|
||||
// GetByEmail if not found, return nil for user but not error.
|
||||
func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.UserEntity, error) {
|
||||
userDB, err := ur.querier.GetUserByEmail(ctx, email)
|
||||
func (u *userRepository) GetByEmail(
|
||||
ctx context.Context,
|
||||
email string,
|
||||
tx any,
|
||||
) (*model.UserEntity, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(u.queries, tx)
|
||||
|
||||
userDB, err := queries.GetUserByEmail(timeoutCtx, email)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// No query error, but user not found
|
||||
return nil, nil
|
||||
@ -107,8 +107,13 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ur *userRepository) GetByID(ctx context.Context, id int) (*model.UserEntity, error) {
|
||||
userDB, err := ur.querier.GetUserByID(ctx, int32(id))
|
||||
func (u *userRepository) GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
queries := getQueries(u.queries, tx)
|
||||
|
||||
userDB, err := queries.GetUserByID(timeoutCtx, int32(id))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// No query error, but user not found
|
||||
return nil, nil
|
||||
|
@ -36,7 +36,7 @@ type EventCreateRequest struct {
|
||||
// }}}
|
||||
// {{{ Response View Object (from service to controller)
|
||||
|
||||
type EventBaseItemResponse struct {
|
||||
type EventListResponse struct {
|
||||
ID int
|
||||
Name string
|
||||
Description string
|
||||
@ -57,7 +57,7 @@ type EventInfoResponse struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
Users []*UserBaseResponse
|
||||
Users []UserBaseResponse
|
||||
}
|
||||
|
||||
// }}}
|
||||
@ -93,7 +93,7 @@ type EventRetrieved struct {
|
||||
Name string
|
||||
Description string
|
||||
|
||||
Users []*UserBaseRetrieved
|
||||
Users []UserBaseRetrieved
|
||||
|
||||
TotalAmount Money
|
||||
DefaultCurrency Currency
|
||||
@ -121,9 +121,9 @@ type Event struct {
|
||||
Description string
|
||||
|
||||
// lazy get using participation join
|
||||
Users []*UserDO
|
||||
Users []UserDO
|
||||
// lazy get
|
||||
Expenses []*Expense
|
||||
Expenses []Expense
|
||||
|
||||
TotalAmount Money
|
||||
DefaultCurrency Currency
|
||||
|
@ -37,7 +37,17 @@ type ExpenseRequest struct {
|
||||
// }}}
|
||||
// {{{ Response
|
||||
|
||||
type ExpensesListResponse struct {
|
||||
type (
|
||||
ExpenseGetResponse Expense
|
||||
ExpenseResponse ExpenseRetrieved
|
||||
)
|
||||
|
||||
// }}}
|
||||
// {{{ Retrieved
|
||||
|
||||
type ExpenseRetrieved Expense
|
||||
|
||||
type ExpensesListRetrieved struct {
|
||||
ID int `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@ -48,12 +58,21 @@ type ExpensesListResponse struct {
|
||||
Detail ExpenseDetail `json:"detail"`
|
||||
}
|
||||
|
||||
type ExpenseGetResponse Expense
|
||||
type PaymentRetrieved struct {
|
||||
PayerID int `json:"payer_id"`
|
||||
PayerFirstName string `json:"payer_first_name"`
|
||||
PayerLastName string `json:"payer_last_name"`
|
||||
Amount int `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Retrieved
|
||||
|
||||
type ExpensesListRetrieved ExpensesListResponse
|
||||
type BenefitRetrieved struct {
|
||||
RecipientID int `json:"recipient_id"`
|
||||
RecipientFirstName string `json:"recipient_first_name"`
|
||||
RecipientLastName string `json:"recipient_last_name"`
|
||||
Amount int `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Entity
|
||||
@ -72,6 +91,18 @@ type ExpenseEntity struct {
|
||||
Place string
|
||||
}
|
||||
|
||||
type ExpenseUpdateEntity struct {
|
||||
ID int
|
||||
UpdatedAt time.Time
|
||||
|
||||
Amount int
|
||||
Currency string
|
||||
|
||||
// Expense Detail
|
||||
Name string
|
||||
Place string
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Domain Models
|
||||
|
||||
|
@ -29,34 +29,46 @@ import (
|
||||
)
|
||||
|
||||
type EventRepository interface {
|
||||
Create(ctx context.Context, evEntity *model.EventEntity) (*model.EventEntity, error)
|
||||
Create(ctx context.Context, evEntity *model.EventEntity, tx any) (*model.EventEntity, error)
|
||||
|
||||
// UpdateEventByID updates the event related information (name, descriptions)
|
||||
UpdateEventByID(ctx context.Context, event *model.EventUpdateEntity) error
|
||||
UpdateEventByID(ctx context.Context, event *model.EventUpdateEntity, tx any) error
|
||||
|
||||
GetByID(ctx context.Context, eventID int) (*model.EventRetrieved, error)
|
||||
GetByID(ctx context.Context, eventID int, tx any) (*model.EventRetrieved, error)
|
||||
|
||||
// related to events of a user
|
||||
ListEventsByUserID(ctx context.Context, userID int) ([]*model.EventListRetrieved, error)
|
||||
ListEventsByUserID(ctx context.Context, userID int, tx any) ([]model.EventListRetrieved, error)
|
||||
|
||||
InsertParticipation(
|
||||
ctx context.Context,
|
||||
userID, eventID, invitedByUserID int,
|
||||
tx any,
|
||||
) (*model.ParticipationEntity, error)
|
||||
|
||||
GetParticipation(
|
||||
ctx context.Context,
|
||||
userID, eventID int,
|
||||
tx any,
|
||||
) (*model.ParticipationEntity, error)
|
||||
}
|
||||
|
||||
type ExpenseRepository interface {
|
||||
Create()
|
||||
Update()
|
||||
Delete() // Delete also the related transactions
|
||||
|
||||
ListExpensesByUserID()
|
||||
GetByID()
|
||||
}
|
||||
|
||||
type ParticipationRepository interface {
|
||||
Create()
|
||||
Delete()
|
||||
CheckParticipation(ctx context.Context, userID, eventID int) error
|
||||
}
|
||||
|
||||
type TransactionRepository interface {
|
||||
Create()
|
||||
// Delete() might be handled in the Expense
|
||||
// Transaction is a joined entity, we don't provide diret read operation
|
||||
DeleteExpense(ctx context.Context, expenseID int, tx any) error
|
||||
DeleteTransactionsOfExpense(ctx context.Context, expenseID int, tx any) error
|
||||
GetExpenseByID(ctx context.Context, expenseID int, tx any) (*model.ExpenseRetrieved, error)
|
||||
InsertExpense(
|
||||
ctx context.Context,
|
||||
expenseEntity *model.ExpenseEntity,
|
||||
tx any,
|
||||
) (*model.ExpenseEntity, error)
|
||||
ListExpensesByEventID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
tx any,
|
||||
) ([]model.ExpensesListRetrieved, error)
|
||||
UpdateExpenseByID(
|
||||
ctx context.Context,
|
||||
expenseUpdate *model.ExpenseUpdateEntity,
|
||||
tx any,
|
||||
) (*model.ExpenseEntity, error)
|
||||
}
|
||||
|
@ -31,9 +31,9 @@ import (
|
||||
type UserRepository interface {
|
||||
Create(
|
||||
ctx context.Context,
|
||||
transaction interface{},
|
||||
u *model.UserEntity,
|
||||
tx any,
|
||||
) (*model.UserEntity, error)
|
||||
GetByEmail(ctx context.Context, email string) (*model.UserEntity, error)
|
||||
GetByID(ctx context.Context, id int) (*model.UserEntity, error)
|
||||
GetByEmail(ctx context.Context, email string, tx any) (*model.UserEntity, error)
|
||||
GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error)
|
||||
}
|
||||
|
@ -33,11 +33,9 @@ import (
|
||||
)
|
||||
|
||||
type eventUsecase struct {
|
||||
userUC User
|
||||
eventRepo repo.EventRepository
|
||||
expenseRepo repo.ExpenseRepository
|
||||
participationRepo repo.ParticipationRepository
|
||||
transactionRepo repo.TransactionRepository
|
||||
userUC User
|
||||
eventRepo repo.EventRepository
|
||||
expenseRepo repo.ExpenseRepository
|
||||
|
||||
dbRepo repo.DBRepository
|
||||
}
|
||||
@ -55,18 +53,16 @@ func NewEventUsecase(
|
||||
uuc User,
|
||||
ev repo.EventRepository,
|
||||
ex repo.ExpenseRepository,
|
||||
pa repo.ParticipationRepository, // XXX: Might be handled in event
|
||||
tr repo.TransactionRepository, // XXX: Might be handled in event
|
||||
db repo.DBRepository,
|
||||
) Event {
|
||||
return &eventUsecase{uuc, ev, ex, pa, tr, db}
|
||||
return &eventUsecase{uuc, ev, ex, db}
|
||||
}
|
||||
|
||||
func (evuc *eventUsecase) CreateEvent(
|
||||
ctx context.Context,
|
||||
evRequest *model.EventCreateRequest,
|
||||
) (*model.EventInfoResponse, error) {
|
||||
// transfer evRequest to PO
|
||||
// transfer evRequest to evEntity
|
||||
|
||||
evEntity := &model.EventEntity{
|
||||
Name: evRequest.Name,
|
||||
@ -78,12 +74,37 @@ func (evuc *eventUsecase) CreateEvent(
|
||||
|
||||
data, err := evuc.dbRepo.Transaction(
|
||||
ctx,
|
||||
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
||||
created, err := evuc.eventRepo.Create(ctx, evEntity)
|
||||
func(txCtx context.Context, tx any) (any, error) {
|
||||
// Create the event
|
||||
created, err := evuc.eventRepo.Create(ctx, evEntity, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// participate to the event
|
||||
participation, err := evuc.eventRepo.InsertParticipation(
|
||||
ctx,
|
||||
created.OwnerID,
|
||||
created.ID,
|
||||
0,
|
||||
tx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if participation == nil {
|
||||
// Unexpected error
|
||||
log.ErrorLog(
|
||||
"participation existed for event-user pair",
|
||||
"userID",
|
||||
created.OwnerID,
|
||||
"eventID",
|
||||
created.ID,
|
||||
)
|
||||
return nil, errno.InternalServerErr
|
||||
}
|
||||
|
||||
// TODO: App log, maybe can be sent to some third party service.
|
||||
log.InfoLog(
|
||||
"created new event",
|
||||
@ -93,6 +114,7 @@ func (evuc *eventUsecase) CreateEvent(
|
||||
created.OwnerID,
|
||||
)
|
||||
|
||||
// Construct the response
|
||||
ownerResponse, err := evuc.userUC.GetUserBaseResponseByID(ctx, created.OwnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -108,9 +130,12 @@ func (evuc *eventUsecase) CreateEvent(
|
||||
),
|
||||
Owner: ownerResponse,
|
||||
CreatedAt: created.CreatedAt,
|
||||
UpdatedAt: created.UpdatedAt,
|
||||
Users: []model.UserBaseResponse{*ownerResponse},
|
||||
}
|
||||
return evResponse, err
|
||||
})
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -123,8 +148,28 @@ func (evuc *eventUsecase) CreateEvent(
|
||||
func (evuc *eventUsecase) ListEvents(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
) ([]model.EventBaseItemResponse, error) {
|
||||
return nil, nil
|
||||
) ([]model.EventListResponse, error) {
|
||||
eventListRetrieved, err := evuc.eventRepo.ListEventsByUserID(ctx, userID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the user is a member of the event
|
||||
|
||||
responses := make([]model.EventListResponse, len(eventListRetrieved))
|
||||
|
||||
for i, retrieved := range eventListRetrieved {
|
||||
ownner := model.UserBaseResponse(*retrieved.Owner)
|
||||
res := model.EventListResponse{
|
||||
ID: retrieved.ID,
|
||||
Name: retrieved.Name,
|
||||
Description: retrieved.Description,
|
||||
Owner: &ownner,
|
||||
CreatedAt: retrieved.CreatedAt,
|
||||
}
|
||||
responses[i] = res
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// GetEventDetail
|
||||
@ -133,10 +178,10 @@ func (evuc *eventUsecase) GetEventDetail(
|
||||
userID, eventID int,
|
||||
) (*model.EventInfoResponse, error) {
|
||||
// Check if the user has the right to get this event
|
||||
err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID)
|
||||
if err != nil {
|
||||
return nil, ErrNoParticipation
|
||||
}
|
||||
// err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID)
|
||||
// if err != nil {
|
||||
// return nil, ErrNoParticipation
|
||||
// }
|
||||
|
||||
// Get the eventDetail
|
||||
// TODO: This can also be put into the cache
|
||||
|
@ -36,8 +36,8 @@ type TestUserRepository struct{}
|
||||
|
||||
func (tur *TestUserRepository) Create(
|
||||
ctx context.Context,
|
||||
transaction interface{},
|
||||
u *model.UserEntity,
|
||||
tx any,
|
||||
) (*model.UserEntity, error) {
|
||||
user := *u
|
||||
|
||||
@ -53,6 +53,7 @@ func (tur *TestUserRepository) Create(
|
||||
func (tur *TestUserRepository) GetByEmail(
|
||||
ctx context.Context,
|
||||
email string,
|
||||
tx any,
|
||||
) (*model.UserEntity, error) {
|
||||
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
||||
switch email {
|
||||
@ -71,7 +72,11 @@ func (tur *TestUserRepository) GetByEmail(
|
||||
return nil, UserTestDummyErr
|
||||
}
|
||||
|
||||
func (tur *TestUserRepository) GetByID(ctx context.Context, id int) (*model.UserEntity, error) {
|
||||
func (tur *TestUserRepository) GetByID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
tx any,
|
||||
) (*model.UserEntity, error) {
|
||||
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
||||
switch id {
|
||||
case 123:
|
||||
|
@ -86,12 +86,12 @@ func (uuc *userUsecase) Create(
|
||||
data, err := uuc.dbRepo.Transaction(
|
||||
ctx,
|
||||
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
||||
created, err := uuc.userRepo.Create(txCtx, tx, &model.UserEntity{
|
||||
created, err := uuc.userRepo.Create(txCtx, &model.UserEntity{
|
||||
Email: u.Email,
|
||||
Password: u.Password,
|
||||
FirstName: u.FirstName,
|
||||
LastName: u.LastName,
|
||||
})
|
||||
}, tx)
|
||||
if err != nil {
|
||||
match, _ := regexp.MatchString("SQLSTATE 23505", err.Error())
|
||||
if match {
|
||||
@ -132,7 +132,7 @@ func (uuc *userUsecase) Create(
|
||||
}
|
||||
|
||||
func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error {
|
||||
got, err := uuc.userRepo.GetByEmail(ctx, u.Email)
|
||||
got, err := uuc.userRepo.GetByEmail(ctx, u.Email, nil)
|
||||
// Any query error?
|
||||
if err != nil {
|
||||
return err
|
||||
@ -160,7 +160,7 @@ func (uuc *userUsecase) GetUserBaseResponseByID(
|
||||
// If not exists, get from the DB. And then put back
|
||||
// into the cache with a timeout.
|
||||
// Refresh the cache when the user data is updated (for now it cannot be updated)
|
||||
got, err := uuc.userRepo.GetByID(ctx, userID)
|
||||
got, err := uuc.userRepo.GetByID(ctx, userID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
Reference in New Issue
Block a user