Compare commits

...

6 Commits

Author SHA1 Message Date
dd999b9355 refacto: refacto repo layer code while adding new usecase methods
All checks were successful
Build and test / Build (push) Successful in 2m19s
2024-10-26 17:27:33 +02:00
14ee642aab refacto: add db tx as a possible input for repo methods
All checks were successful
Build and test / Build (push) Successful in 2m22s
2024-10-25 23:52:43 +02:00
b30a5c5c2d feat: implement repo expense methods
All checks were successful
Build and test / Build (push) Successful in 2m28s
2024-10-24 23:39:13 +02:00
58cff774e6 feat: add expense repo type conversion 2024-10-24 22:33:24 +02:00
716a58d44c fix: use simple slices instead of []*T 2024-10-24 22:32:57 +02:00
de7c6f7223 test: add event repo tests 2024-10-23 23:29:44 +02:00
19 changed files with 869 additions and 150 deletions

View File

@ -564,3 +564,21 @@ querier.
Since this repo layer is just a wrapping layer between the `sqlc.models` and 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. 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. 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?

View File

@ -25,7 +25,9 @@ package repo
import ( import (
"context" "context"
"database/sql" "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/howmuch/usecase/repo"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
) )
@ -34,6 +36,8 @@ type dbRepository struct {
db *sql.DB db *sql.DB
} }
const queryTimeout = 3 * time.Second
func NewDBRepository(db *sql.DB) repo.DBRepository { func NewDBRepository(db *sql.DB) repo.DBRepository {
return &dbRepository{ return &dbRepository{
db: db, db: db,
@ -66,3 +70,11 @@ func (dr *dbRepository) Transaction(
data, err := txFunc(ctx, tx) data, err := txFunc(ctx, tx)
return data, err 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
}

View File

@ -26,6 +26,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors"
"time" "time"
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc" "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
@ -35,7 +36,7 @@ import (
) )
type eventRepository struct { type eventRepository struct {
queries sqlc.Querier queries *sqlc.Queries
} }
func NewEventRepository(db *sql.DB) repo.EventRepository { func NewEventRepository(db *sql.DB) repo.EventRepository {
@ -48,8 +49,35 @@ func NewEventRepository(db *sql.DB) repo.EventRepository {
func (e *eventRepository) Create( func (e *eventRepository) Create(
ctx context.Context, ctx context.Context,
evEntity *model.EventEntity, evEntity *model.EventEntity,
tx any,
) (*model.EventEntity, error) { ) (*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) { func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved, error) {
@ -62,7 +90,7 @@ func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved
return nil, err return nil, err
} }
var users []*model.UserBaseRetrieved var users []model.UserBaseRetrieved
err = json.Unmarshal(eventDTO.Users, &users) err = json.Unmarshal(eventDTO.Users, &users)
if err != nil { if err != nil {
// Unexpected // Unexpected
@ -89,8 +117,17 @@ func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved
} }
// GetByID implements repo.EventRepository. // GetByID implements repo.EventRepository.
func (e *eventRepository) GetByID(ctx context.Context, eventID int) (*model.EventRetrieved, error) { func (e *eventRepository) GetByID(
eventDTO, err := e.queries.GetEventByID(ctx, int32(eventID)) 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 { if err != nil {
log.ErrorLog("query error", "err", err) log.ErrorLog("query error", "err", err)
return nil, err return nil, err
@ -99,10 +136,10 @@ func (e *eventRepository) GetByID(ctx context.Context, eventID int) (*model.Even
return convToEventRetrieved(&eventDTO) return convToEventRetrieved(&eventDTO)
} }
func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]*model.EventListRetrieved, error) { func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]model.EventListRetrieved, error) {
var events []*model.EventListRetrieved events := make([]model.EventListRetrieved, len(eventsDTO))
for _, evDTO := range eventsDTO { for i, evDTO := range eventsDTO {
var owner model.UserBaseRetrieved var owner model.UserBaseRetrieved
err := json.Unmarshal(evDTO.Owner, &owner) err := json.Unmarshal(evDTO.Owner, &owner)
if err != nil { if err != nil {
@ -110,13 +147,15 @@ func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]*model.EventList
log.ErrorLog("json unmarshal error", "err", err) log.ErrorLog("json unmarshal error", "err", err)
return nil, err return nil, err
} }
ev := &model.EventListRetrieved{ ev := model.EventListRetrieved{
ID: int(evDTO.ID), ID: int(evDTO.ID),
Name: evDTO.Name, Name: evDTO.Name,
Description: evDTO.Description.String, Description: evDTO.Description.String,
Owner: &owner,
CreatedAt: evDTO.CreatedAt,
} }
events = append(events, ev) events[i] = ev
} }
return events, nil return events, nil
@ -126,8 +165,14 @@ func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]*model.EventList
func (e *eventRepository) ListEventsByUserID( func (e *eventRepository) ListEventsByUserID(
ctx context.Context, ctx context.Context,
userID int, userID int,
) ([]*model.EventListRetrieved, error) { tx any,
eventsDTO, err := e.queries.ListEventsByUserID(ctx, int32(userID)) ) ([]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 { if err != nil {
log.ErrorLog("query error", "err", err) log.ErrorLog("query error", "err", err)
return nil, err return nil, err
@ -140,8 +185,14 @@ func (e *eventRepository) ListEventsByUserID(
func (e *eventRepository) UpdateEventByID( func (e *eventRepository) UpdateEventByID(
ctx context.Context, ctx context.Context,
event *model.EventUpdateEntity, event *model.EventUpdateEntity,
tx any,
) error { ) 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), ID: int32(event.ID),
Name: event.Name, Name: event.Name,
Description: sql.NullString{String: event.Description, Valid: true}, Description: sql.NullString{String: event.Description, Valid: true},
@ -149,3 +200,77 @@ func (e *eventRepository) UpdateEventByID(
}) })
return err 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
}

View 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)
}

View 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
}

View 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)
}

View File

@ -24,14 +24,6 @@ FROM "expense" ex
JOIN "event" ev ON ev.id = ex.event_id JOIN "event" ev ON ev.id = ex.event_id
WHERE ev.id = $1; 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 -- name: GetExpenseByID :one
WITH payer_transaction as ( WITH payer_transaction as (
SELECT pt.expense_id, SELECT pt.expense_id,

View File

@ -183,47 +183,6 @@ func (q *Queries) ListExpensesByEventID(ctx context.Context, id int32) ([]Expens
return items, nil 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 const updateExpenseByID = `-- name: UpdateExpenseByID :one
UPDATE "expense" UPDATE "expense"
SET updated_at = $2, amount = $3, currency = $4, name = $5, place = $6 SET updated_at = $2, amount = $3, currency = $4, name = $5, place = $6

View File

@ -3,3 +3,8 @@ INSERT INTO participation (
user_id, event_id, invited_by_user_id, created_at, updated_at user_id, event_id, invited_by_user_id, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5) ) VALUES ($1, $2, $3, $4, $5)
RETURNING *; 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;

View File

@ -11,6 +11,31 @@ import (
"time" "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 const insertParticipation = `-- name: InsertParticipation :one
INSERT INTO participation ( INSERT INTO participation (
user_id, event_id, invited_by_user_id, created_at, updated_at user_id, event_id, invited_by_user_id, created_at, updated_at

View File

@ -13,6 +13,7 @@ type Querier interface {
DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error
GetEventByID(ctx context.Context, id int32) (GetEventByIDRow, error) GetEventByID(ctx context.Context, id int32) (GetEventByIDRow, error)
GetExpenseByID(ctx context.Context, id int32) (GetExpenseByIDRow, 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) GetUserByEmail(ctx context.Context, email string) (User, error)
GetUserByID(ctx context.Context, id int32) (User, error) GetUserByID(ctx context.Context, id int32) (User, error)
InsertEvent(ctx context.Context, arg InsertEventParams) (Event, error) InsertEvent(ctx context.Context, arg InsertEventParams) (Event, error)
@ -22,7 +23,6 @@ type Querier interface {
InsertUser(ctx context.Context, arg InsertUserParams) (User, error) InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
ListEventsByUserID(ctx context.Context, userID int32) ([]ListEventsByUserIDRow, error) ListEventsByUserID(ctx context.Context, userID int32) ([]ListEventsByUserIDRow, error)
ListExpensesByEventID(ctx context.Context, id int32) ([]Expense, error) ListExpensesByEventID(ctx context.Context, id int32) ([]Expense, error)
ListExpensesByEventIDByUserID(ctx context.Context, id int32) ([]Expense, error)
UpdateEventByID(ctx context.Context, arg UpdateEventByIDParams) error UpdateEventByID(ctx context.Context, arg UpdateEventByIDParams) error
UpdateExpenseByID(ctx context.Context, arg UpdateExpenseByIDParams) (Expense, error) UpdateExpenseByID(ctx context.Context, arg UpdateExpenseByIDParams) (Expense, error)
} }

View File

@ -34,43 +34,34 @@ import (
) )
type userRepository struct { type userRepository struct {
querier sqlc.Querier queries *sqlc.Queries
} }
const insertTimeout = 1 * time.Second
func NewUserRepository(db *sql.DB) repo.UserRepository { func NewUserRepository(db *sql.DB) repo.UserRepository {
return &userRepository{ return &userRepository{
querier: sqlc.New(db), queries: sqlc.New(db),
} }
} }
// Create // Create
func (ur *userRepository) Create( func (u *userRepository) Create(
ctx context.Context, ctx context.Context,
transaction interface{}, userEntity *model.UserEntity,
u *model.UserEntity, tx any,
) (*model.UserEntity, error) { ) (*model.UserEntity, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, insertTimeout) timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
defer cancel() defer cancel()
args := sqlc.InsertUserParams{ queries := getQueries(u.queries, tx)
Email: u.Email,
FirstName: u.FirstName, userDB, err := queries.InsertUser(timeoutCtx, sqlc.InsertUserParams{
LastName: u.LastName, Email: userEntity.Email,
Password: u.Password, FirstName: userEntity.FirstName,
LastName: userEntity.LastName,
Password: userEntity.Password,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: 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 { if err != nil {
return nil, err return nil, err
} }
@ -87,8 +78,17 @@ func (ur *userRepository) Create(
} }
// GetByEmail if not found, return nil for user but not error. // GetByEmail if not found, return nil for user but not error.
func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.UserEntity, error) { func (u *userRepository) GetByEmail(
userDB, err := ur.querier.GetUserByEmail(ctx, email) 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) { if errors.Is(err, sql.ErrNoRows) {
// No query error, but user not found // No query error, but user not found
return nil, nil return nil, nil
@ -107,8 +107,13 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.
}, nil }, nil
} }
func (ur *userRepository) GetByID(ctx context.Context, id int) (*model.UserEntity, error) { func (u *userRepository) GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error) {
userDB, err := ur.querier.GetUserByID(ctx, int32(id)) 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) { if errors.Is(err, sql.ErrNoRows) {
// No query error, but user not found // No query error, but user not found
return nil, nil return nil, nil

View File

@ -36,7 +36,7 @@ type EventCreateRequest struct {
// }}} // }}}
// {{{ Response View Object (from service to controller) // {{{ Response View Object (from service to controller)
type EventBaseItemResponse struct { type EventListResponse struct {
ID int ID int
Name string Name string
Description string Description string
@ -57,7 +57,7 @@ type EventInfoResponse struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Users []*UserBaseResponse Users []UserBaseResponse
} }
// }}} // }}}
@ -93,7 +93,7 @@ type EventRetrieved struct {
Name string Name string
Description string Description string
Users []*UserBaseRetrieved Users []UserBaseRetrieved
TotalAmount Money TotalAmount Money
DefaultCurrency Currency DefaultCurrency Currency
@ -121,9 +121,9 @@ type Event struct {
Description string Description string
// lazy get using participation join // lazy get using participation join
Users []*UserDO Users []UserDO
// lazy get // lazy get
Expenses []*Expense Expenses []Expense
TotalAmount Money TotalAmount Money
DefaultCurrency Currency DefaultCurrency Currency

View File

@ -37,7 +37,17 @@ type ExpenseRequest struct {
// }}} // }}}
// {{{ Response // {{{ Response
type ExpensesListResponse struct { type (
ExpenseGetResponse Expense
ExpenseResponse ExpenseRetrieved
)
// }}}
// {{{ Retrieved
type ExpenseRetrieved Expense
type ExpensesListRetrieved struct {
ID int `json:"id"` ID int `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -48,12 +58,21 @@ type ExpensesListResponse struct {
Detail ExpenseDetail `json:"detail"` 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"`
}
// }}} type BenefitRetrieved struct {
// {{{ Retrieved RecipientID int `json:"recipient_id"`
RecipientFirstName string `json:"recipient_first_name"`
type ExpensesListRetrieved ExpensesListResponse RecipientLastName string `json:"recipient_last_name"`
Amount int `json:"amount"`
Currency string `json:"currency"`
}
// }}} // }}}
// {{{ Entity // {{{ Entity
@ -72,6 +91,18 @@ type ExpenseEntity struct {
Place string Place string
} }
type ExpenseUpdateEntity struct {
ID int
UpdatedAt time.Time
Amount int
Currency string
// Expense Detail
Name string
Place string
}
// }}} // }}}
// {{{ Domain Models // {{{ Domain Models

View File

@ -29,34 +29,46 @@ import (
) )
type EventRepository interface { 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 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 // 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 { type ExpenseRepository interface {
Create() DeleteExpense(ctx context.Context, expenseID int, tx any) error
Update() DeleteTransactionsOfExpense(ctx context.Context, expenseID int, tx any) error
Delete() // Delete also the related transactions GetExpenseByID(ctx context.Context, expenseID int, tx any) (*model.ExpenseRetrieved, error)
InsertExpense(
ListExpensesByUserID() ctx context.Context,
GetByID() expenseEntity *model.ExpenseEntity,
} tx any,
) (*model.ExpenseEntity, error)
type ParticipationRepository interface { ListExpensesByEventID(
Create() ctx context.Context,
Delete() id int,
CheckParticipation(ctx context.Context, userID, eventID int) error tx any,
} ) ([]model.ExpensesListRetrieved, error)
UpdateExpenseByID(
type TransactionRepository interface { ctx context.Context,
Create() expenseUpdate *model.ExpenseUpdateEntity,
// Delete() might be handled in the Expense tx any,
// Transaction is a joined entity, we don't provide diret read operation ) (*model.ExpenseEntity, error)
} }

View File

@ -31,9 +31,9 @@ import (
type UserRepository interface { type UserRepository interface {
Create( Create(
ctx context.Context, ctx context.Context,
transaction interface{},
u *model.UserEntity, u *model.UserEntity,
tx any,
) (*model.UserEntity, error) ) (*model.UserEntity, error)
GetByEmail(ctx context.Context, email string) (*model.UserEntity, error) GetByEmail(ctx context.Context, email string, tx any) (*model.UserEntity, error)
GetByID(ctx context.Context, id int) (*model.UserEntity, error) GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error)
} }

View File

@ -33,11 +33,9 @@ import (
) )
type eventUsecase struct { type eventUsecase struct {
userUC User userUC User
eventRepo repo.EventRepository eventRepo repo.EventRepository
expenseRepo repo.ExpenseRepository expenseRepo repo.ExpenseRepository
participationRepo repo.ParticipationRepository
transactionRepo repo.TransactionRepository
dbRepo repo.DBRepository dbRepo repo.DBRepository
} }
@ -55,18 +53,16 @@ func NewEventUsecase(
uuc User, uuc User,
ev repo.EventRepository, ev repo.EventRepository,
ex repo.ExpenseRepository, ex repo.ExpenseRepository,
pa repo.ParticipationRepository, // XXX: Might be handled in event
tr repo.TransactionRepository, // XXX: Might be handled in event
db repo.DBRepository, db repo.DBRepository,
) Event { ) Event {
return &eventUsecase{uuc, ev, ex, pa, tr, db} return &eventUsecase{uuc, ev, ex, db}
} }
func (evuc *eventUsecase) CreateEvent( func (evuc *eventUsecase) CreateEvent(
ctx context.Context, ctx context.Context,
evRequest *model.EventCreateRequest, evRequest *model.EventCreateRequest,
) (*model.EventInfoResponse, error) { ) (*model.EventInfoResponse, error) {
// transfer evRequest to PO // transfer evRequest to evEntity
evEntity := &model.EventEntity{ evEntity := &model.EventEntity{
Name: evRequest.Name, Name: evRequest.Name,
@ -78,12 +74,37 @@ func (evuc *eventUsecase) CreateEvent(
data, err := evuc.dbRepo.Transaction( data, err := evuc.dbRepo.Transaction(
ctx, ctx,
func(txCtx context.Context, tx interface{}) (interface{}, error) { func(txCtx context.Context, tx any) (any, error) {
created, err := evuc.eventRepo.Create(ctx, evEntity) // Create the event
created, err := evuc.eventRepo.Create(ctx, evEntity, tx)
if err != nil { if err != nil {
return nil, err 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. // TODO: App log, maybe can be sent to some third party service.
log.InfoLog( log.InfoLog(
"created new event", "created new event",
@ -93,6 +114,7 @@ func (evuc *eventUsecase) CreateEvent(
created.OwnerID, created.OwnerID,
) )
// Construct the response
ownerResponse, err := evuc.userUC.GetUserBaseResponseByID(ctx, created.OwnerID) ownerResponse, err := evuc.userUC.GetUserBaseResponseByID(ctx, created.OwnerID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -108,9 +130,12 @@ func (evuc *eventUsecase) CreateEvent(
), ),
Owner: ownerResponse, Owner: ownerResponse,
CreatedAt: created.CreatedAt, CreatedAt: created.CreatedAt,
UpdatedAt: created.UpdatedAt,
Users: []model.UserBaseResponse{*ownerResponse},
} }
return evResponse, err return evResponse, err
}) },
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -123,8 +148,28 @@ func (evuc *eventUsecase) CreateEvent(
func (evuc *eventUsecase) ListEvents( func (evuc *eventUsecase) ListEvents(
ctx context.Context, ctx context.Context,
userID int, userID int,
) ([]model.EventBaseItemResponse, error) { ) ([]model.EventListResponse, error) {
return nil, nil 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 // GetEventDetail
@ -133,10 +178,10 @@ func (evuc *eventUsecase) GetEventDetail(
userID, eventID int, userID, eventID int,
) (*model.EventInfoResponse, error) { ) (*model.EventInfoResponse, error) {
// Check if the user has the right to get this event // Check if the user has the right to get this event
err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID) // err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID)
if err != nil { // if err != nil {
return nil, ErrNoParticipation // return nil, ErrNoParticipation
} // }
// Get the eventDetail // Get the eventDetail
// TODO: This can also be put into the cache // TODO: This can also be put into the cache

View File

@ -36,8 +36,8 @@ type TestUserRepository struct{}
func (tur *TestUserRepository) Create( func (tur *TestUserRepository) Create(
ctx context.Context, ctx context.Context,
transaction interface{},
u *model.UserEntity, u *model.UserEntity,
tx any,
) (*model.UserEntity, error) { ) (*model.UserEntity, error) {
user := *u user := *u
@ -53,6 +53,7 @@ func (tur *TestUserRepository) Create(
func (tur *TestUserRepository) GetByEmail( func (tur *TestUserRepository) GetByEmail(
ctx context.Context, ctx context.Context,
email string, email string,
tx any,
) (*model.UserEntity, error) { ) (*model.UserEntity, error) {
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12) hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
switch email { switch email {
@ -71,7 +72,11 @@ func (tur *TestUserRepository) GetByEmail(
return nil, UserTestDummyErr 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) hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
switch id { switch id {
case 123: case 123:

View File

@ -86,12 +86,12 @@ func (uuc *userUsecase) Create(
data, err := uuc.dbRepo.Transaction( data, err := uuc.dbRepo.Transaction(
ctx, ctx,
func(txCtx context.Context, tx interface{}) (interface{}, error) { 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, Email: u.Email,
Password: u.Password, Password: u.Password,
FirstName: u.FirstName, FirstName: u.FirstName,
LastName: u.LastName, LastName: u.LastName,
}) }, tx)
if err != nil { if err != nil {
match, _ := regexp.MatchString("SQLSTATE 23505", err.Error()) match, _ := regexp.MatchString("SQLSTATE 23505", err.Error())
if match { if match {
@ -132,7 +132,7 @@ func (uuc *userUsecase) Create(
} }
func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error { 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? // Any query error?
if err != nil { if err != nil {
return err return err
@ -160,7 +160,7 @@ func (uuc *userUsecase) GetUserBaseResponseByID(
// If not exists, get from the DB. And then put back // If not exists, get from the DB. And then put back
// into the cache with a timeout. // into the cache with a timeout.
// Refresh the cache when the user data is updated (for now it cannot be updated) // 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 { if err != nil {
return nil, err return nil, err
} }