Skip to content

Commit 17cb069

Browse files
committed
Create billing usage repository
1 parent 84369d2 commit 17cb069

5 files changed

Lines changed: 198 additions & 1 deletion

File tree

api/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/gofiber/swagger v0.0.1
1717
github.com/google/uuid v1.3.0
1818
github.com/hirosassa/zerodriver v0.1.2
19+
github.com/jinzhu/now v1.1.4
1920
github.com/joho/godotenv v1.4.0
2021
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
2122
github.com/matcornic/hermes/v2 v2.1.0
@@ -84,7 +85,6 @@ require (
8485
github.com/jackc/pgx/v4 v4.16.1 // indirect
8586
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect
8687
github.com/jinzhu/inflection v1.0.0 // indirect
87-
github.com/jinzhu/now v1.1.4 // indirect
8888
github.com/josharian/intern v1.0.0 // indirect
8989
github.com/json-iterator/go v1.1.10 // indirect
9090
github.com/klauspost/compress v1.15.5 // indirect

api/pkg/di/container.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strconv"
88
"time"
99

10+
"github.com/jinzhu/now"
11+
1012
"github.com/uptrace/uptrace-go/uptrace"
1113

1214
"github.com/NdoleStudio/httpsms/pkg/emails"
@@ -61,6 +63,9 @@ type Container struct {
6163

6264
// NewContainer creates a new dependency injection container
6365
func NewContainer(projectID string) (container *Container) {
66+
// Set location to UTC
67+
now.DefaultConfig.TimeLocation = time.UTC
68+
6469
container = &Container{
6570
projectID: projectID,
6671
logger: logger(3).WithService(fmt.Sprintf("%T", container)),
@@ -205,6 +210,10 @@ func (container *Container) DB() (db *gorm.DB) {
205210
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.PhoneNotification{})))
206211
}
207212

213+
if err = db.AutoMigrate(&entities.BillingUsage{}); err != nil {
214+
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.BillingUsage{})))
215+
}
216+
208217
return container.db
209218
}
210219

@@ -399,6 +408,16 @@ func (container *Container) PhoneRepository() (repository repositories.PhoneRepo
399408
)
400409
}
401410

411+
// BillingUsageRepository creates a new instance of repositories.BillingUsageRepository
412+
func (container *Container) BillingUsageRepository() (repository repositories.BillingUsageRepository) {
413+
container.logger.Debug("creating GORM repositories.BillingUsageRepository")
414+
return repositories.NewGormBillingUsageRepository(
415+
container.Logger(),
416+
container.Tracer(),
417+
container.DB(),
418+
)
419+
}
420+
402421
// PhoneNotificationRepository creates a new instance of repositories.PhoneNotificationRepository
403422
func (container *Container) PhoneNotificationRepository() (repository repositories.PhoneNotificationRepository) {
404423
container.logger.Debug("creating GORM repositories.PhoneNotificationRepository")

api/pkg/entities/billing_usage.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// BillingUsage tracks the billing usage of an account
10+
type BillingUsage struct {
11+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
12+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
13+
SentMessages uint `json:"sent_messages" example:"321"`
14+
ReceivedMessages uint `json:"received_messages" example:"465"`
15+
StartTimestamp time.Time `json:"start_timestamp" example:"2022-01-01T00:00:00+00:00"`
16+
EndTimestamp time.Time `json:"end_timestamp" example:"2022-01-31T23:59:59+00:00"`
17+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
18+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
)
8+
9+
// BillingUsageRepository loads and persists an entities.BillingUsage
10+
type BillingUsageRepository interface {
11+
// RegisterSentMessage registers a message as sent
12+
RegisterSentMessage(ctx context.Context, user entities.UserID) error
13+
14+
// RegisterReceivedMessage registers a message as received
15+
RegisterReceivedMessage(ctx context.Context, user entities.UserID) error
16+
17+
// GetCurrent returns the current billing usage by entities.UserID
18+
GetCurrent(ctx context.Context, userID entities.UserID) (*entities.BillingUsage, error)
19+
20+
// GetHistory returns past billing usage by entities.UserID
21+
GetHistory(ctx context.Context, userID entities.UserID, params IndexParams) (*[]entities.BillingUsage, error)
22+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/NdoleStudio/httpsms/pkg/entities"
9+
"github.com/NdoleStudio/httpsms/pkg/telemetry"
10+
"github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm"
11+
"github.com/google/uuid"
12+
"github.com/jinzhu/now"
13+
"github.com/palantir/stacktrace"
14+
"gorm.io/gorm"
15+
)
16+
17+
// gormBillingUsageRepository is responsible for persisting entities.BillingUsage
18+
type gormBillingUsageRepository struct {
19+
logger telemetry.Logger
20+
tracer telemetry.Tracer
21+
db *gorm.DB
22+
}
23+
24+
// NewGormBillingUsageRepository creates the GORM version of the BillingUsageRepository
25+
func NewGormBillingUsageRepository(
26+
logger telemetry.Logger,
27+
tracer telemetry.Tracer,
28+
db *gorm.DB,
29+
) BillingUsageRepository {
30+
return &gormBillingUsageRepository{
31+
logger: logger.WithService(fmt.Sprintf("%T", &gormBillingUsageRepository{})),
32+
tracer: tracer,
33+
db: db,
34+
}
35+
}
36+
37+
// RegisterSentMessage registers a message as sent
38+
func (repository *gormBillingUsageRepository) RegisterSentMessage(ctx context.Context, userID entities.UserID) error {
39+
ctx, span := repository.tracer.Start(ctx)
40+
defer span.End()
41+
42+
return crdbgorm.ExecuteTx(ctx, repository.db, nil,
43+
func(tx *gorm.DB) error {
44+
result := tx.WithContext(ctx).
45+
Model(&entities.BillingUsage{}).
46+
Where("start_timestamp = ?", now.BeginningOfMonth()).
47+
Where("user_id = ?", userID).
48+
UpdateColumn("sent_messages", gorm.Expr("sent_messages + ?", 1))
49+
50+
if result.RowsAffected == 0 {
51+
return tx.Create(repository.createBillingUsage(userID, 1, 0)).Error
52+
}
53+
return result.Error
54+
},
55+
)
56+
}
57+
58+
// RegisterReceivedMessage registers a message as received
59+
func (repository *gormBillingUsageRepository) RegisterReceivedMessage(ctx context.Context, userID entities.UserID) error {
60+
ctx, span := repository.tracer.Start(ctx)
61+
defer span.End()
62+
63+
return crdbgorm.ExecuteTx(ctx, repository.db, nil,
64+
func(tx *gorm.DB) error {
65+
result := tx.WithContext(ctx).
66+
Model(&entities.BillingUsage{}).
67+
Where("start_timestamp = ?", now.BeginningOfMonth()).
68+
Where("user_id = ?", userID).
69+
UpdateColumn("sent_messages", gorm.Expr("received_messages + ?", 1))
70+
71+
if result.RowsAffected == 0 {
72+
return tx.Create(repository.createBillingUsage(userID, 0, 1)).Error
73+
}
74+
return result.Error
75+
},
76+
)
77+
}
78+
79+
// GetCurrent returns the current billing usage by entities.UserID
80+
func (repository *gormBillingUsageRepository) GetCurrent(ctx context.Context, userID entities.UserID) (*entities.BillingUsage, error) {
81+
ctx, span := repository.tracer.Start(ctx)
82+
defer span.End()
83+
84+
usage := repository.createBillingUsage(userID, 0, 0)
85+
86+
err := crdbgorm.ExecuteTx(ctx, repository.db, nil,
87+
func(tx *gorm.DB) error {
88+
result := tx.WithContext(ctx).
89+
Where("start_timestamp = ?", now.BeginningOfMonth()).
90+
First(usage)
91+
92+
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
93+
return tx.WithContext(ctx).Create(usage).Error
94+
}
95+
return result.Error
96+
},
97+
)
98+
if err != nil {
99+
return usage, stacktrace.Propagate(err, fmt.Sprintf("cannot load billing usage for user [%s]", userID))
100+
}
101+
102+
return usage, err
103+
}
104+
105+
// GetHistory returns past billing usage by entities.UserID
106+
func (repository *gormBillingUsageRepository) GetHistory(ctx context.Context, userID entities.UserID, params IndexParams) (*[]entities.BillingUsage, error) {
107+
ctx, span := repository.tracer.Start(ctx)
108+
defer span.End()
109+
110+
usages := new([]entities.BillingUsage)
111+
112+
err := repository.db.WithContext(ctx).
113+
Where("user_id = ?", userID).
114+
Where("start_timestamp != ?", now.BeginningOfMonth()).
115+
Order("start_timestamp DESC").
116+
Limit(params.Limit).
117+
Offset(params.Skip).
118+
Find(&usages).
119+
Error
120+
if err != nil {
121+
msg := fmt.Sprintf("cannot fetch billing usage history for userID [%s] and params [%+#v]", userID, params)
122+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
123+
}
124+
125+
return usages, err
126+
}
127+
128+
func (repository *gormBillingUsageRepository) createBillingUsage(userID entities.UserID, sent uint, received uint) *entities.BillingUsage {
129+
return &entities.BillingUsage{
130+
ID: uuid.New(),
131+
UserID: userID,
132+
SentMessages: sent,
133+
ReceivedMessages: received,
134+
StartTimestamp: now.BeginningOfMonth(),
135+
EndTimestamp: now.EndOfMonth(),
136+
}
137+
}

0 commit comments

Comments
 (0)