diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 0638f10..7f8eca6 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -161,6 +161,7 @@ CREATE TABLE IF NOT EXISTS transactions ( account_number VARCHAR(255) NOT NULL, reference_number VARCHAR(255) NOT NULL, verified BOOLEAN NOT NULL DEFAULT false, + approved_by BIGINT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -243,7 +244,6 @@ CREATE TABLE odds ( UNIQUE (event_id, market_id, name, handicap), UNIQUE (event_id, market_id) ); - CREATE TABLE companies ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, @@ -397,4 +397,4 @@ VALUES ( TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP - ); + ); \ No newline at end of file diff --git a/db/query/transactions.sql b/db/query/transactions.sql index a5d21b0..a64363f 100644 --- a/db/query/transactions.sql +++ b/db/query/transactions.sql @@ -1,16 +1,51 @@ -- name: CreateTransaction :one -INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *; - +INSERT INTO transactions ( + amount, + branch_id, + cashier_id, + bet_id, + type, + payment_option, + full_name, + phone_number, + bank_code, + beneficiary_name, + account_name, + account_number, + reference_number, + number_of_outcomes + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14 + ) +RETURNING *; -- name: GetAllTransactions :many -SELECT * FROM transactions; - +SELECT * +FROM transactions; -- name: GetTransactionByID :one -SELECT * FROM transactions WHERE id = $1; - +SELECT * +FROM transactions +WHERE id = $1; -- name: GetTransactionByBranch :many -SELECT * FROM transactions WHERE branch_id = $1; - +SELECT * +FROM transactions +WHERE branch_id = $1; -- name: UpdateTransactionVerified :exec -UPDATE transactions SET verified = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; - - +UPDATE transactions +SET verified = $2, + approved_by = $3, + updated_at = CURRENT_TIMESTAMP +WHERE id = $1; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index d7a5bad..116db7f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -55,7 +55,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/handlers.AdminRes" } }, "400": { @@ -1449,7 +1449,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/handlers.ManagersRes" } }, "400": { @@ -3445,7 +3445,7 @@ const docTemplate = `{ "OUTCOME_STATUS_PENDING", "OUTCOME_STATUS_WIN", "OUTCOME_STATUS_LOSS", - "OUTCOME_STATUS_ERROR" + "OUTCOME_STATUS_VOID" ] }, "domain.PaymentOption": { @@ -3703,6 +3703,50 @@ const docTemplate = `{ } } }, + "handlers.AdminRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.BetRes": { "type": "object", "properties": { @@ -3851,9 +3895,6 @@ const docTemplate = `{ }, "handlers.CheckPhoneEmailExistReq": { "type": "object", - "required": [ - "phone_number" - ], "properties": { "email": { "type": "string", @@ -3971,10 +4012,6 @@ const docTemplate = `{ } ], "example": 1 - }, - "total_odds": { - "type": "number", - "example": 4.22 } } }, @@ -4140,10 +4177,6 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/handlers.CreateTicketOutcomeReq" } - }, - "total_odds": { - "type": "number", - "example": 4.22 } } }, @@ -4267,6 +4300,50 @@ const docTemplate = `{ } } }, + "handlers.ManagersRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.RegisterCodeReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 7be8f14..131ed58 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -47,7 +47,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/handlers.AdminRes" } }, "400": { @@ -1441,7 +1441,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/handlers.ManagersRes" } }, "400": { @@ -3437,7 +3437,7 @@ "OUTCOME_STATUS_PENDING", "OUTCOME_STATUS_WIN", "OUTCOME_STATUS_LOSS", - "OUTCOME_STATUS_ERROR" + "OUTCOME_STATUS_VOID" ] }, "domain.PaymentOption": { @@ -3695,6 +3695,50 @@ } } }, + "handlers.AdminRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.BetRes": { "type": "object", "properties": { @@ -3843,9 +3887,6 @@ }, "handlers.CheckPhoneEmailExistReq": { "type": "object", - "required": [ - "phone_number" - ], "properties": { "email": { "type": "string", @@ -3963,10 +4004,6 @@ } ], "example": 1 - }, - "total_odds": { - "type": "number", - "example": 4.22 } } }, @@ -4132,10 +4169,6 @@ "items": { "$ref": "#/definitions/handlers.CreateTicketOutcomeReq" } - }, - "total_odds": { - "type": "number", - "example": 4.22 } } }, @@ -4259,6 +4292,50 @@ } } }, + "handlers.ManagersRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.RegisterCodeReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 243a29a..28c76fc 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -90,7 +90,7 @@ definitions: - OUTCOME_STATUS_PENDING - OUTCOME_STATUS_WIN - OUTCOME_STATUS_LOSS - - OUTCOME_STATUS_ERROR + - OUTCOME_STATUS_VOID domain.PaymentOption: enum: - 0 @@ -272,6 +272,35 @@ definitions: description: Converted from "time" field in UNIX format type: string type: object + handlers.AdminRes: + properties: + created_at: + type: string + email: + type: string + email_verified: + type: boolean + first_name: + type: string + id: + type: integer + last_login: + type: string + last_name: + type: string + phone_number: + type: string + phone_verified: + type: boolean + role: + $ref: '#/definitions/domain.Role' + suspended: + type: boolean + suspended_at: + type: string + updated_at: + type: string + type: object handlers.BetRes: properties: amount: @@ -384,8 +413,6 @@ definitions: phone_number: example: "1234567890" type: string - required: - - phone_number type: object handlers.CheckPhoneEmailExistRes: properties: @@ -461,9 +488,6 @@ definitions: allOf: - $ref: '#/definitions/domain.OutcomeStatus' example: 1 - total_odds: - example: 4.22 - type: number type: object handlers.CreateBranchOperationReq: properties: @@ -581,9 +605,6 @@ definitions: items: $ref: '#/definitions/handlers.CreateTicketOutcomeReq' type: array - total_odds: - example: 4.22 - type: number type: object handlers.CreateTicketRes: properties: @@ -668,6 +689,35 @@ definitions: static_updated_at: type: string type: object + handlers.ManagersRes: + properties: + created_at: + type: string + email: + type: string + email_verified: + type: boolean + first_name: + type: string + id: + type: integer + last_login: + type: string + last_name: + type: string + phone_number: + type: string + phone_verified: + type: boolean + role: + $ref: '#/definitions/domain.Role' + suspended: + type: boolean + suspended_at: + type: string + updated_at: + type: string + type: object handlers.RegisterCodeReq: properties: email: @@ -1055,7 +1105,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/handlers.AdminRes' "400": description: Bad Request schema: @@ -1975,7 +2025,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/handlers.ManagersRes' "400": description: Bad Request schema: diff --git a/gen/db/models.go b/gen/db/models.go index c0ef0d7..820d65a 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -350,6 +350,7 @@ type Transaction struct { AccountNumber string `json:"account_number"` ReferenceNumber string `json:"reference_number"` Verified bool `json:"verified"` + ApprovedBy pgtype.Int8 `json:"approved_by"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` } diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index b4aec9d..6c605a5 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -7,26 +7,61 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) const CreateTransaction = `-- name: CreateTransaction :one -INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at +INSERT INTO transactions ( + amount, + branch_id, + cashier_id, + bet_id, + type, + payment_option, + full_name, + phone_number, + bank_code, + beneficiary_name, + account_name, + account_number, + reference_number, + number_of_outcomes + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14 + ) +RETURNING id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at ` type CreateTransactionParams struct { - Amount int64 `json:"amount"` - BranchID int64 `json:"branch_id"` - CashierID int64 `json:"cashier_id"` - BetID int64 `json:"bet_id"` - Type int64 `json:"type"` - PaymentOption int64 `json:"payment_option"` - FullName string `json:"full_name"` - PhoneNumber string `json:"phone_number"` - BankCode string `json:"bank_code"` - BeneficiaryName string `json:"beneficiary_name"` - AccountName string `json:"account_name"` - AccountNumber string `json:"account_number"` - ReferenceNumber string `json:"reference_number"` + Amount int64 `json:"amount"` + BranchID int64 `json:"branch_id"` + CashierID int64 `json:"cashier_id"` + BetID int64 `json:"bet_id"` + Type int64 `json:"type"` + PaymentOption int64 `json:"payment_option"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + BankCode string `json:"bank_code"` + BeneficiaryName string `json:"beneficiary_name"` + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + ReferenceNumber string `json:"reference_number"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` } func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) { @@ -44,6 +79,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa arg.AccountName, arg.AccountNumber, arg.ReferenceNumber, + arg.NumberOfOutcomes, ) var i Transaction err := row.Scan( @@ -63,6 +99,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa &i.AccountNumber, &i.ReferenceNumber, &i.Verified, + &i.ApprovedBy, &i.CreatedAt, &i.UpdatedAt, ) @@ -70,7 +107,8 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa } const GetAllTransactions = `-- name: GetAllTransactions :many -SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions +SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at +FROM transactions ` func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) { @@ -99,6 +137,7 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) &i.AccountNumber, &i.ReferenceNumber, &i.Verified, + &i.ApprovedBy, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -113,7 +152,9 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) } const GetTransactionByBranch = `-- name: GetTransactionByBranch :many -SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE branch_id = $1 +SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at +FROM transactions +WHERE branch_id = $1 ` func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([]Transaction, error) { @@ -142,6 +183,7 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([ &i.AccountNumber, &i.ReferenceNumber, &i.Verified, + &i.ApprovedBy, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -156,7 +198,9 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([ } const GetTransactionByID = `-- name: GetTransactionByID :one -SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE id = $1 +SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at +FROM transactions +WHERE id = $1 ` func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction, error) { @@ -179,6 +223,7 @@ func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction &i.AccountNumber, &i.ReferenceNumber, &i.Verified, + &i.ApprovedBy, &i.CreatedAt, &i.UpdatedAt, ) @@ -186,15 +231,20 @@ func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction } const UpdateTransactionVerified = `-- name: UpdateTransactionVerified :exec -UPDATE transactions SET verified = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 +UPDATE transactions +SET verified = $2, + approved_by = $3, + updated_at = CURRENT_TIMESTAMP +WHERE id = $1 ` type UpdateTransactionVerifiedParams struct { - ID int64 `json:"id"` - Verified bool `json:"verified"` + ID int64 `json:"id"` + Verified bool `json:"verified"` + ApprovedBy pgtype.Int8 `json:"approved_by"` } func (q *Queries) UpdateTransactionVerified(ctx context.Context, arg UpdateTransactionVerifiedParams) error { - _, err := q.db.Exec(ctx, UpdateTransactionVerified, arg.ID, arg.Verified) + _, err := q.db.Exec(ctx, UpdateTransactionVerified, arg.ID, arg.Verified, arg.ApprovedBy) return err } diff --git a/internal/domain/transaction.go b/internal/domain/transaction.go index 8abd19b..781610f 100644 --- a/internal/domain/transaction.go +++ b/internal/domain/transaction.go @@ -1,5 +1,7 @@ package domain +import "time" + type TransactionType int const ( @@ -36,6 +38,9 @@ type Transaction struct { AccountNumber string ReferenceNumber string Verified bool + ApprovedBy ValidInt64 + UpdatedAt time.Time + CreatedAt time.Time } type CreateTransaction struct { diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index 3526d74..e0753b2 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -5,10 +5,12 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction { return domain.Transaction{ + ID: transaction.ID, Amount: domain.Currency(transaction.Amount), BranchID: transaction.BranchID, CashierID: transaction.CashierID, @@ -23,24 +25,32 @@ func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction { AccountName: transaction.AccountName, AccountNumber: transaction.AccountNumber, ReferenceNumber: transaction.ReferenceNumber, + ApprovedBy: domain.ValidInt64{ + Value: transaction.ApprovedBy.Int64, + Valid: transaction.ApprovedBy.Valid, + }, + CreatedAt: transaction.CreatedAt.Time, + UpdatedAt: transaction.UpdatedAt.Time, + Verified: transaction.Verified, } } func convertCreateTransaction(transaction domain.CreateTransaction) dbgen.CreateTransactionParams { return dbgen.CreateTransactionParams{ - Amount: int64(transaction.Amount), - BranchID: transaction.BranchID, - CashierID: transaction.CashierID, - BetID: transaction.BetID, - Type: int64(transaction.Type), - PaymentOption: int64(transaction.PaymentOption), - FullName: transaction.FullName, - PhoneNumber: transaction.PhoneNumber, - BankCode: transaction.BankCode, - BeneficiaryName: transaction.BeneficiaryName, - AccountName: transaction.AccountName, - AccountNumber: transaction.AccountNumber, - ReferenceNumber: transaction.ReferenceNumber, + Amount: int64(transaction.Amount), + BranchID: transaction.BranchID, + CashierID: transaction.CashierID, + BetID: transaction.BetID, + Type: int64(transaction.Type), + PaymentOption: int64(transaction.PaymentOption), + FullName: transaction.FullName, + PhoneNumber: transaction.PhoneNumber, + BankCode: transaction.BankCode, + BeneficiaryName: transaction.BeneficiaryName, + AccountName: transaction.AccountName, + AccountNumber: transaction.AccountNumber, + ReferenceNumber: transaction.ReferenceNumber, + NumberOfOutcomes: transaction.NumberOfOutcomes, } } @@ -89,9 +99,13 @@ func (s *Store) GetTransactionByBranch(ctx context.Context, id int64) ([]domain. return result, nil } -func (s *Store) UpdateTransactionVerified(ctx context.Context, id int64, verified bool) error { +func (s *Store) UpdateTransactionVerified(ctx context.Context, id int64, verified bool, approvedBy int64) error { err := s.queries.UpdateTransactionVerified(ctx, dbgen.UpdateTransactionVerifiedParams{ - ID: id, + ID: id, + ApprovedBy: pgtype.Int8{ + Int64: approvedBy, + Valid: true, + }, Verified: verified, }) return err diff --git a/internal/services/transaction/port.go b/internal/services/transaction/port.go index cbd9a0f..052ce45 100644 --- a/internal/services/transaction/port.go +++ b/internal/services/transaction/port.go @@ -11,5 +11,5 @@ type TransactionStore interface { GetTransactionByID(ctx context.Context, id int64) (domain.Transaction, error) GetAllTransactions(ctx context.Context) ([]domain.Transaction, error) GetTransactionByBranch(ctx context.Context, id int64) ([]domain.Transaction, error) - UpdateTransactionVerified(ctx context.Context, id int64, verified bool) error + UpdateTransactionVerified(ctx context.Context, id int64, verified bool, approvedBy int64) error } diff --git a/internal/services/transaction/service.go b/internal/services/transaction/service.go index 2c33917..3e965cc 100644 --- a/internal/services/transaction/service.go +++ b/internal/services/transaction/service.go @@ -28,7 +28,6 @@ func (s *Service) GetAllTransactions(ctx context.Context) ([]domain.Transaction, func (s *Service) GetTransactionByBranch(ctx context.Context, id int64) ([]domain.Transaction, error) { return s.transactionStore.GetTransactionByBranch(ctx, id) } -func (s *Service) UpdateTransactionVerified(ctx context.Context, id int64, verified bool) error { - - return s.transactionStore.UpdateTransactionVerified(ctx, id, verified) +func (s *Service) UpdateTransactionVerified(ctx context.Context, id int64, verified bool, approvedBy int64) error { + return s.transactionStore.UpdateTransactionVerified(ctx, id, verified, approvedBy) } diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 1cfbaee..7378791 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -5,6 +5,7 @@ import ( "context" "log" + "time" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" @@ -25,6 +26,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { log.Printf("FetchUpcomingEvents error: %v", err) } + time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour }, }, @@ -36,18 +38,17 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // } // }, // }, - // { - // // spec: "0 */15 * * * *", // Every 15 minutes - // spec: "0 0 * * * *", // TODO: Every hour because of the 3600 requests per hour limit - // task: func() { + { + // spec: "0 */15 * * * *", // Every 15 minutes + spec: "0 0 * * * *", // TODO: Every hour because of the 3600 requests per hour limit + task: func() { + if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + log.Printf("FetchNonLiveOdds error: %v", err) + } + time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // log.Printf("FetchNonLiveOdds error: %v", err) - // } - // time.Sleep(2 * time.Second) //This will restrict the fetching to 1800 requests per hour - - // }, - // }, + }, + }, // { // spec: "0 */15 * * * *", // task: func() { @@ -71,7 +72,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { - job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { log.Fatalf("Failed to schedule cron job: %v", err) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index a3c5df1..5019602 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -18,10 +18,10 @@ type CreateBetOutcomeReq struct { type CreateBetReq struct { Outcomes []CreateBetOutcomeReq `json:"outcomes"` Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` Status domain.OutcomeStatus `json:"status" example:"1"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` + BranchID *int64 `json:"branch_id,omitempty" example:"1"` } type CreateBetRes struct { @@ -99,6 +99,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { // Get user_id from middleware userID := c.Locals("user_id").(int64) + role := c.Locals("role").(domain.Role) var req CreateBetReq if err := c.BodyParser(&req); err != nil { @@ -111,109 +112,13 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - // Validating user by role - // Differentiating between offline and online bets - user, err := h.userSvc.GetUserByID(c.Context(), userID) - if err != nil { - h.logger.Error("CreateBetReq failed, user id invalid") - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) - } - cashoutID, err := h.betSvc.GenerateCashoutID() - if err != nil { - h.logger.Error("CreateBetReq failed, unable to create cashout id") - return response.WriteJSON(c, fiber.StatusInternalServerError, "Invalid request", err, nil) - } - var bet domain.Bet - if user.Role == domain.RoleCashier { - - // Get the branch from the branch ID - branch, err := h.branchSvc.GetBranchByCashier(c.Context(), user.ID) - if err != nil { - h.logger.Error("CreateBetReq failed, branch id invalid") - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) - } - - // Deduct a percentage of the amount - // TODO move to service layer. Make it fetch dynamically from company - var deductedAmount = req.Amount / 10 - err = h.walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.ToCurrency(deductedAmount)) - - if err != nil { - h.logger.Error("CreateBetReq failed, unable to deduct from WalletID") - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) - } - - bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ - Amount: domain.ToCurrency(req.Amount), - TotalOdds: req.TotalOdds, - Status: req.Status, - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - - BranchID: domain.ValidInt64{ - Value: branch.ID, - Valid: true, - }, - UserID: domain.ValidInt64{ - Value: userID, - Valid: false, - }, - IsShopBet: true, - CashoutID: cashoutID, - }) - } else if user.Role == domain.RoleSuperAdmin { - // This is just for testing - bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ - Amount: domain.ToCurrency(req.Amount), - TotalOdds: req.TotalOdds, - Status: req.Status, - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - BranchID: domain.ValidInt64{ - Value: 1, - Valid: true, - }, - UserID: domain.ValidInt64{ - Value: userID, - Valid: true, - }, - IsShopBet: true, - CashoutID: cashoutID, - }) - } else { - // TODO if user is customer, get id from the token then get the wallet id from there and reduce the amount - bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ - Amount: domain.ToCurrency(req.Amount), - TotalOdds: req.TotalOdds, - Status: req.Status, - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - - BranchID: domain.ValidInt64{ - Value: 0, - Valid: false, - }, - UserID: domain.ValidInt64{ - Value: userID, - Valid: true, - }, - IsShopBet: false, - CashoutID: cashoutID, - }) - } - - if err != nil { - h.logger.Error("CreateBetReq failed", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) - } - - // // TODO Validate Outcomes Here and make sure they didn't expire // Validation for creating tickets if len(req.Outcomes) > 30 { return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) } var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes)) + var totalOdds float32 = 1 for _, outcome := range req.Outcomes { eventIDStr := strconv.FormatInt(outcome.EventID, 10) marketIDStr := strconv.FormatInt(outcome.MarketID, 10) @@ -262,9 +167,9 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { } parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) + totalOdds = totalOdds * float32(parsedOdd) outcomes = append(outcomes, domain.CreateBetOutcome{ - BetID: bet.ID, EventID: outcome.EventID, OddID: outcome.OddID, MarketID: outcome.MarketID, @@ -279,6 +184,108 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { }) } + // Validating user by role + // Differentiating between offline and online bets + cashoutID, err := h.betSvc.GenerateCashoutID() + if err != nil { + h.logger.Error("CreateBetReq failed, unable to create cashout id") + return response.WriteJSON(c, fiber.StatusInternalServerError, "Invalid request", err, nil) + } + var bet domain.Bet + if role == domain.RoleCashier { + + // Get the branch from the branch ID + branch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) + if err != nil { + h.logger.Error("CreateBetReq failed, branch id invalid") + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + // Deduct a percentage of the amount + // TODO move to service layer. Make it fetch dynamically from company + var deductedAmount = req.Amount / 10 + err = h.walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.ToCurrency(deductedAmount)) + + if err != nil { + h.logger.Error("CreateBetReq failed, unable to deduct from WalletID") + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ + Amount: domain.ToCurrency(req.Amount), + TotalOdds: totalOdds, + Status: req.Status, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + + BranchID: domain.ValidInt64{ + Value: branch.ID, + Valid: true, + }, + UserID: domain.ValidInt64{ + Value: userID, + Valid: false, + }, + IsShopBet: true, + CashoutID: cashoutID, + }) + } else if role == domain.RoleSuperAdmin || role == domain.RoleAdmin || role == domain.RoleBranchManager { + // If a non cashier wants to create a bet, they will need to provide the Branch ID + // TODO: restrict the Branch ID of Admin and Branch Manager to only the branches within their own company + if req.BranchID == nil { + h.logger.Error("CreateBetReq failed, Branch ID is required for this type of user") + return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID is required for this type of user", nil, nil) + } + // h.logger.Info("Branch ID", slog.Int64("branch_id", *req.BranchID)) + bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ + Amount: domain.ToCurrency(req.Amount), + TotalOdds: totalOdds, + Status: req.Status, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + BranchID: domain.ValidInt64{ + Value: *req.BranchID, + Valid: true, + }, + UserID: domain.ValidInt64{ + Value: userID, + Valid: true, + }, + IsShopBet: true, + CashoutID: cashoutID, + }) + } else { + // TODO if user is customer, get id from the token then get the wallet id from there and reduce the amount + bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ + Amount: domain.ToCurrency(req.Amount), + TotalOdds: totalOdds, + Status: req.Status, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + + BranchID: domain.ValidInt64{ + Value: 0, + Valid: false, + }, + UserID: domain.ValidInt64{ + Value: userID, + Valid: true, + }, + IsShopBet: false, + CashoutID: cashoutID, + }) + } + + if err != nil { + h.logger.Error("CreateBetReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) + } + + // Updating the bet id for outcomes + for index := range outcomes { + outcomes[index].BetID = bet.ID + } + rows, err := h.betSvc.CreateBetOutcome(c.Context(), outcomes) if err != nil { diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index f12ca8a..f91d0f2 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -23,9 +23,8 @@ type CreateTicketOutcomeReq struct { } type CreateTicketReq struct { - Outcomes []CreateTicketOutcomeReq `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` + Outcomes []CreateTicketOutcomeReq `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` } type CreateTicketRes struct { FastCode int64 `json:"fast_code" example:"1234"` @@ -66,6 +65,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) } var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) + var totalOdds float32 = 1 for _, outcome := range req.Outcomes { eventIDStr := strconv.FormatInt(outcome.EventID, 10) marketIDStr := strconv.FormatInt(outcome.MarketID, 10) @@ -100,7 +100,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { rawBytes, err := json.Marshal(raw) err = json.Unmarshal(rawBytes, &rawOdd) if err != nil { - h.logger.Error("Failed to unmarshal raw odd:", err) + h.logger.Error("Failed to unmarshal raw odd:", "error", err) continue } if rawOdd.ID == oddIDStr { @@ -114,7 +114,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { } parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) - + totalOdds = totalOdds * float32(parsedOdd) outcomes = append(outcomes, domain.CreateTicketOutcome{ EventID: outcome.EventID, OddID: outcome.OddID, @@ -129,10 +129,9 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { Expires: event.StartTime, }) } - ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ Amount: domain.ToCurrency(req.Amount), - TotalOdds: req.TotalOdds, + TotalOdds: totalOdds, }) if err != nil { h.logger.Error("CreateTicketReq failed", "error", err) diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index 99c323b..5263803 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -1,7 +1,9 @@ package handlers import ( + "log/slog" "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -25,6 +27,9 @@ type TransactionRes struct { AccountNumber string `json:"account_number"` ReferenceNumber string `json:"reference_number"` Verified bool `json:"verified" example:"true"` + ApprovedBy *int64 `json:"approved_by" example:"1"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` } type CreateTransactionReq struct { @@ -40,26 +45,36 @@ type CreateTransactionReq struct { AccountName string `json:"account_name"` AccountNumber string `json:"account_number"` ReferenceNumber string `json:"reference_number"` + BranchID *int64 `json:"branch_id,omitempty" example:"1"` } func convertTransaction(transaction domain.Transaction) TransactionRes { - return TransactionRes{ - ID: transaction.ID, - Amount: transaction.Amount.Float32(), - BranchID: transaction.BranchID, - CashierID: transaction.CashierID, - BetID: transaction.BetID, - Type: int64(transaction.Type), - PaymentOption: transaction.PaymentOption, - FullName: transaction.FullName, - PhoneNumber: transaction.PhoneNumber, - BankCode: transaction.BankCode, - BeneficiaryName: transaction.BeneficiaryName, - AccountName: transaction.AccountName, - AccountNumber: transaction.AccountNumber, - ReferenceNumber: transaction.ReferenceNumber, - Verified: transaction.Verified, + newTransaction := TransactionRes{ + ID: transaction.ID, + Amount: transaction.Amount.Float32(), + BranchID: transaction.BranchID, + CashierID: transaction.CashierID, + BetID: transaction.BetID, + Type: int64(transaction.Type), + PaymentOption: transaction.PaymentOption, + FullName: transaction.FullName, + PhoneNumber: transaction.PhoneNumber, + BankCode: transaction.BankCode, + BeneficiaryName: transaction.BeneficiaryName, + AccountName: transaction.AccountName, + AccountNumber: transaction.AccountNumber, + ReferenceNumber: transaction.ReferenceNumber, + Verified: transaction.Verified, + NumberOfOutcomes: transaction.NumberOfOutcomes, + CreatedAt: transaction.CreatedAt, + UpdatedAt: transaction.UpdatedAt, } + + if transaction.ApprovedBy.Valid { + newTransaction.ApprovedBy = &transaction.ApprovedBy.Value + } + + return newTransaction } // CreateTransaction godoc @@ -75,35 +90,17 @@ func convertTransaction(transaction domain.Transaction) TransactionRes { // @Router /transaction [post] func (h *Handler) CreateTransaction(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) - user, err := h.userSvc.GetUserByID(c.Context(), userID) + role := c.Locals("role").(domain.Role) + // user, err := h.userSvc.GetUserByID(c.Context(), userID) - if user.Role == domain.RoleCustomer { - h.logger.Error("CreateTransactionReq failed") + // TODO: Make a "Only Company" middleware auth and move this into that + if role == domain.RoleCustomer { + h.logger.Error("CreateTransactionReq failed due to unauthorized access") return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "unauthorized access", }) } - // TODO: Add validation to make sure that the bet hasn't already been cashed out by someone else - var branchID int64 - if user.Role == domain.RoleAdmin || user.Role == domain.RoleBranchManager || user.Role == domain.RoleSuperAdmin { - branch, err := h.branchSvc.GetBranchByID(c.Context(), 1) - if err != nil { - h.logger.Error("CreateTransactionReq no branches") - return response.WriteJSON(c, fiber.StatusBadRequest, "This user type doesn't have branches", err, nil) - } - - branchID = branch.ID - - } else { - branch, err := h.branchSvc.GetBranchByCashier(c.Context(), user.ID) - if err != nil { - h.logger.Error("CreateTransactionReq failed, branch id invalid") - return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID invalid", err, nil) - } - branchID = branch.ID - } - var req CreateTransactionReq if err := c.BodyParser(&req); err != nil { h.logger.Error("CreateTransaction failed to parse request", "error", err) @@ -116,13 +113,51 @@ func (h *Handler) CreateTransaction(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - // TODO: Validate the bet id and add the number of outcomes + var branchID int64 + if role == domain.RoleAdmin || role == domain.RoleBranchManager || role == domain.RoleSuperAdmin { + if req.BranchID == nil { + h.logger.Error("CreateTransactionReq Branch ID is required for this user role") + return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID is required for this user role", nil, nil) + } + branch, err := h.branchSvc.GetBranchByID(c.Context(), *req.BranchID) + if err != nil { + h.logger.Error("CreateTransactionReq no branches") + return response.WriteJSON(c, fiber.StatusBadRequest, "cannot find Branch ID", err, nil) + } + + branchID = branch.ID + + } else { + branch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) + if err != nil { + h.logger.Error("CreateTransactionReq failed, branch id invalid") + return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID invalid", err, nil) + } + branchID = branch.ID + } + + bet, err := h.betSvc.GetBetByID(c.Context(), req.BetID) + if err != nil { + h.logger.Error("CreateTransactionReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Bet ID invalid", err, nil) + } + + // if bet.Status != domain.OUTCOME_STATUS_WIN { + // h.logger.Error("CreateTransactionReq failed, bet has not won") + // return response.WriteJSON(c, fiber.StatusBadRequest, "User has not won bet", err, nil) + // } + + if bet.CashedOut { + h.logger.Error(("Bet has already been cashed out")) + return response.WriteJSON(c, fiber.StatusBadRequest, "This bet has already been cashed out", err, nil) + } + transaction, err := h.transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ BranchID: branchID, CashierID: userID, Amount: domain.ToCurrency(req.Amount), - BetID: req.BetID, - NumberOfOutcomes: 1, + BetID: bet.ID, + NumberOfOutcomes: int64(len(bet.Outcomes)), Type: domain.TransactionType(req.Type), PaymentOption: domain.PaymentOption(req.PaymentOption), FullName: req.FullName, @@ -236,7 +271,7 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error { } type UpdateTransactionVerifiedReq struct { - Verified bool `json:"verified" validate:"required" example:"true"` + Verified bool `json:"verified" example:"true"` } // UpdateTransactionVerified godoc @@ -250,10 +285,12 @@ type UpdateTransactionVerifiedReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /transaction/{id} [patch] +// @Router /transaction/{id} [put] func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error { - transactionID := c.Params("id") + userID := c.Locals("user_id").(int64) + // companyID := c.Locals("company_id").(domain.ValidInt64) + id, err := strconv.ParseInt(transactionID, 10, 64) if err != nil { h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) @@ -266,11 +303,14 @@ func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } + h.logger.Info("Update Transaction Verified", slog.Bool("verified", req.Verified)) + if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - err = h.transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified) + // TODO: make it so that only people within the company can verify a transaction + err = h.transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified, userID) if err != nil { h.logger.Error("Failed to update transaction verification", "transactionID", id, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transaction verification") diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index a70e19b..3ba25fb 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -159,7 +159,7 @@ func (a *App) initAppRoutes() { a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions) a.fiber.Get("/transaction/:id", a.authMiddleware, h.GetTransactionByID) - a.fiber.Patch("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified) + a.fiber.Put("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified) // Notification Routes a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket)