Skip to content

Commit c1df9ba

Browse files
authored
Merge pull request #26 from NLipatov/feature/2
Feature/2
2 parents cfbc091 + f58d0fb commit c1df9ba

29 files changed

+1304
-95
lines changed

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ services:
228228
echo -e 'Creating kafka topics'
229229
kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic PROXY --replication-factor 1 --partitions 1
230230
kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic PLAN --replication-factor 1 --partitions 1
231+
kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic BILLING --replication-factor 1 --partitions 1
231232
232233
echo -e 'Successfully created the following topics:'
233234
kafka-topics --bootstrap-server kafka:9092 --list
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package crypto_cloud_commands
2+
3+
type PostBackCommand struct {
4+
OrderID string `json:"order_id"`
5+
Token string `json:"token"`
6+
Secret string `json:"secret"`
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package crypto_cloud_commands
2+
3+
type IssueInvoiceCommand struct {
4+
AmountUSD float64
5+
Email string
6+
OrderId string
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package crypto_cloud
2+
3+
import (
4+
"goproxy/application/payments/crypto_cloud/crypto_cloud_commands"
5+
)
6+
7+
type PaymentProvider interface {
8+
IssueInvoice(command crypto_cloud_commands.IssueInvoiceCommand) (interface{}, error)
9+
HandlePostBack(command crypto_cloud_commands.PostBackCommand) error
10+
}

src/domain/BoundedContext.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package domain
33
type BoundedContexts string
44

55
const (
6-
UNSET BoundedContexts = "UNSET"
7-
PROXY BoundedContexts = "PROXY"
8-
PLAN BoundedContexts = "PLAN"
6+
UNSET BoundedContexts = "UNSET"
7+
PROXY BoundedContexts = "PROXY"
8+
PLAN BoundedContexts = "PLAN"
9+
BILLING BoundedContexts = "BILLING"
910
)

src/domain/events/order_paid.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package events
2+
3+
type OrderPaidEvent struct {
4+
OrderId string `json:"order_id"`
5+
}
6+
7+
func NewOrderPaidEvent(orderId string) OrderPaidEvent {
8+
return OrderPaidEvent{
9+
OrderId: orderId,
10+
}
11+
}

src/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/confluentinc/confluent-kafka-go v1.9.2
99
github.com/go-redis/redis/v8 v8.11.5
1010
github.com/golang-jwt/jwt/v4 v4.5.1
11+
github.com/google/uuid v1.6.0
1112
github.com/gorilla/websocket v1.5.3
1213
github.com/lib/pq v1.10.9
1314
github.com/shirou/gopsutil v3.21.11+incompatible
@@ -16,6 +17,7 @@ require (
1617
github.com/vmihailenco/msgpack/v5 v5.4.1
1718
golang.org/x/crypto v0.32.0
1819
golang.org/x/oauth2 v0.25.0
20+
golang.org/x/sync v0.10.0
1921
)
2022

2123
require (
@@ -40,7 +42,6 @@ require (
4042
github.com/go-logr/stdr v1.2.2 // indirect
4143
github.com/go-ole/go-ole v1.3.0 // indirect
4244
github.com/gogo/protobuf v1.3.2 // indirect
43-
github.com/google/uuid v1.6.0 // indirect
4445
github.com/klauspost/compress v1.17.11 // indirect
4546
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
4647
github.com/magiconair/properties v1.8.9 // indirect
@@ -72,7 +73,6 @@ require (
7273
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
7374
go.opentelemetry.io/otel/trace v1.33.0 // indirect
7475
golang.org/x/net v0.33.0 // indirect
75-
golang.org/x/sync v0.10.0 // indirect
7676
golang.org/x/sys v0.29.0 // indirect
7777
golang.org/x/time v0.9.0 // indirect
7878
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect

src/infrastructure/api/api-http/accounting/controller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ type Controller struct {
2020
func NewAccountingController(
2121
billingService application.BillingService[lavatopaggregates.Invoice,
2222
lavatopvalueobjects.Offer], planRepository application.PlanRepository,
23-
planOfferRepository application.PlanOfferRepository, lavaTopUseCases application.LavaTopUseCases) *Controller {
24-
handler := lavatop.NewHandler(billingService, planRepository, planOfferRepository, lavaTopUseCases)
23+
planOfferRepository application.PlanOfferRepository, lavaTopUseCases application.LavaTopUseCases, userUseCases application.UserUseCases) *Controller {
24+
handler := lavatop.NewHandler(billingService, planRepository, planOfferRepository, lavaTopUseCases, userUseCases)
2525
return &Controller{
2626
handler: handler,
2727
corsManager: CORS.NewCORSManager(),
@@ -39,7 +39,7 @@ func (c *Controller) Listen(port int) {
3939
case http.MethodGet:
4040
c.handler.GetInvoices(w, r)
4141
case http.MethodPost:
42-
c.handler.PostInvoices(w, r)
42+
c.handler.PostInvoice(w, r)
4343
default:
4444
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
4545
}

src/infrastructure/api/api-http/accounting/lavatop/handler.go

Lines changed: 127 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ package lavatop
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"github.com/golang-jwt/jwt/v4"
67
"goproxy/application"
8+
"goproxy/domain/aggregates"
79
"goproxy/domain/lavatopsubdomain/lavatopaggregates"
810
"goproxy/domain/lavatopsubdomain/lavatopvalueobjects"
911
"goproxy/infrastructure/api/api-http/google_auth"
1012
"goproxy/infrastructure/dto"
13+
"io"
1114
"log"
1215
"net/http"
16+
"os"
1317
)
1418

1519
type Handler struct {
@@ -18,16 +22,22 @@ type Handler struct {
1822
plansRepository application.PlanRepository
1923
planOfferRepository application.PlanOfferRepository
2024
lavaTopUseCases application.LavaTopUseCases
25+
plansResponse PlansResponse
2126
}
2227

2328
func NewHandler(billingService application.BillingService[lavatopaggregates.Invoice, lavatopvalueobjects.Offer],
2429
planRepository application.PlanRepository, planOfferRepository application.PlanOfferRepository,
25-
lavaTopUseCases application.LavaTopUseCases) *Handler {
30+
lavaTopUseCases application.LavaTopUseCases, userUseCases application.UserUseCases) *Handler {
31+
32+
plansResponse := NewPlansResponse(planRepository, lavaTopUseCases, planOfferRepository)
33+
2634
return &Handler{
2735
billingService: billingService,
2836
plansRepository: planRepository,
2937
planOfferRepository: planOfferRepository,
3038
lavaTopUseCases: lavaTopUseCases,
39+
plansResponse: plansResponse,
40+
userUseCases: userUseCases,
3141
}
3242
}
3343

@@ -109,97 +119,131 @@ func (h Handler) GetInvoices(w http.ResponseWriter, r *http.Request) {
109119
panic("not implemented")
110120
}
111121

112-
func (h Handler) PostInvoices(writer http.ResponseWriter, request *http.Request) {
113-
panic("not implemented")
114-
}
122+
func (h Handler) PostInvoice(w http.ResponseWriter, r *http.Request) {
123+
user, userErr := h.getUser(r)
124+
if userErr != nil {
125+
w.WriteHeader(http.StatusUnauthorized)
126+
_ = json.NewEncoder(w).Encode(dto.ApiResponse[dto.PostInvoiceResponse]{
127+
Payload: nil,
128+
ErrorCode: http.StatusUnauthorized,
129+
ErrorMessage: "not authorized",
130+
})
131+
return
132+
}
115133

116-
func (h Handler) GetPlans(w http.ResponseWriter, _ *http.Request) {
117-
response := dto.ApiResponse[[]dto.Plan]{
118-
Payload: nil,
119-
ErrorCode: 0,
120-
ErrorMessage: "",
134+
var cmd dto.AccountingIssueInvoiceCommand
135+
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 512))
136+
if err := decoder.Decode(&cmd); err != nil {
137+
w.WriteHeader(http.StatusBadRequest)
138+
_ = json.NewEncoder(w).Encode(dto.ApiResponse[dto.PostInvoiceResponse]{
139+
Payload: nil,
140+
ErrorCode: http.StatusBadRequest,
141+
ErrorMessage: "invalid body",
142+
})
143+
return
121144
}
122145

123-
plans, plansErr := h.plansRepository.GetAllWithFeatures()
124-
if plansErr != nil {
125-
response.ErrorCode = http.StatusInternalServerError
126-
response.ErrorMessage = "could not load plans"
127-
w.WriteHeader(http.StatusInternalServerError)
146+
currency, currencyErr := lavatopvalueobjects.ParseCurrency(cmd.Currency)
147+
if currencyErr != nil {
148+
w.WriteHeader(http.StatusBadRequest)
149+
_ = json.NewEncoder(w).Encode(dto.ApiResponse[dto.PostInvoiceResponse]{
150+
Payload: nil,
151+
ErrorCode: http.StatusBadRequest,
152+
ErrorMessage: "invalid currency",
153+
})
154+
return
155+
}
156+
157+
paymentMethod, paymentMethodErr := lavatopvalueobjects.ParsePaymentMethod(cmd.PaymentMethod)
158+
if paymentMethodErr != nil {
159+
w.WriteHeader(http.StatusBadRequest)
160+
_ = json.NewEncoder(w).Encode(dto.ApiResponse[dto.PostInvoiceResponse]{
161+
Payload: nil,
162+
ErrorCode: http.StatusBadRequest,
163+
ErrorMessage: "invalid payment method",
164+
})
165+
return
166+
}
167+
168+
newIssueInvoiceResponse := NewIssueInvoiceResponse(h.lavaTopUseCases, user, currency, paymentMethod, cmd.OfferId)
169+
170+
response, responseErr := newIssueInvoiceResponse.Build()
171+
if responseErr != nil {
128172
_ = json.NewEncoder(w).Encode(response)
129173
return
130174
}
131175

132-
planFeatures := make(map[int][]string)
133-
for _, plan := range plans {
134-
features := make([]string, len(plan.Features()))
135-
for fi, feature := range plan.Features() {
136-
features[fi] = feature.Feature()
137-
}
138-
planFeatures[plan.Id()] = features
139-
}
140-
141-
planPrices := make(map[int][]dto.Price)
142-
lavatopOffers, lavatopOffersErr := h.lavaTopUseCases.GetOffers()
143-
if lavatopOffersErr == nil {
144-
145-
for _, plan := range plans {
146-
planOfferIds, offersErr := h.planOfferRepository.GetOffers(plan.Id())
147-
if offersErr != nil {
148-
continue
149-
}
150-
151-
for _, offer := range lavatopOffers {
152-
for _, planOffers := range planOfferIds {
153-
if offer.ExtId() == planOffers.OfferId() {
154-
for _, v := range offer.Prices() {
155-
priceDto := dto.Price{
156-
Currency: v.Currency().String(),
157-
Cents: v.Cents(),
158-
}
159-
planPrices[plan.Id()] = append(planPrices[plan.Id()], priceDto)
160-
}
161-
}
162-
}
163-
}
164-
}
165-
}
166-
167-
planResponses := make([]dto.Plan, len(plans))
168-
for i, plan := range plans {
169-
features := make([]dto.Feature, len(plan.Features()))
170-
for fi, feature := range plan.Features() {
171-
features[fi] = dto.Feature{
172-
Feature: feature.Feature(),
173-
FeatureDescription: feature.Description(),
174-
}
175-
}
176-
177-
planResponses[i] = dto.Plan{
178-
Name: plan.Name(),
179-
Limits: dto.Limits{
180-
Bandwidth: dto.BandwidthLimit{
181-
IsLimited: plan.LimitBytes() != 0,
182-
Used: 0,
183-
Total: plan.LimitBytes(),
184-
},
185-
Connections: dto.ConnectionLimit{
186-
IsLimited: true,
187-
MaxConcurrentConnections: 25,
188-
},
189-
Speed: dto.SpeedLimit{
190-
IsLimited: false,
191-
MaxBytesPerSecond: 125_000_000, // 125_000_000 bytes is 1 Gigabit/s
192-
},
193-
},
194-
Features: features,
195-
DurationDays: plan.DurationDays(),
196-
Prices: planPrices[plan.Id()],
197-
}
198-
}
199-
200-
response.Payload = &planResponses
176+
w.Header().Set("Content-Type", "application/json")
177+
w.WriteHeader(http.StatusOK)
178+
_ = json.NewEncoder(w).Encode(response)
179+
}
180+
181+
func (h Handler) GetPlans(w http.ResponseWriter, _ *http.Request) {
182+
response, responseErr := h.plansResponse.Build()
183+
if responseErr != nil {
184+
w.WriteHeader(http.StatusInternalServerError)
185+
_ = json.NewEncoder(w).Encode(dto.ApiResponse[[]dto.Plan]{
186+
Payload: nil,
187+
ErrorCode: http.StatusInternalServerError,
188+
ErrorMessage: "could not load plans",
189+
})
190+
return
191+
}
201192

202193
w.Header().Set("Content-Type", "application/json")
203194
w.WriteHeader(http.StatusOK)
204195
_ = json.NewEncoder(w).Encode(response)
205196
}
197+
198+
func (h Handler) getUser(r *http.Request) (aggregates.User, error) {
199+
idToken, err := google_auth.GetIdTokenFromCookie(r)
200+
if err != nil {
201+
return aggregates.User{}, err
202+
}
203+
204+
verifiedToken, err := google_auth.VerifyIDToken(idToken)
205+
if err != nil {
206+
return aggregates.User{}, fmt.Errorf("failed to verify token: %w", err)
207+
}
208+
209+
claims, ok := verifiedToken.Claims.(jwt.MapClaims)
210+
if !ok {
211+
return aggregates.User{}, fmt.Errorf("failed to parse token claims")
212+
}
213+
214+
email := claims["email"].(string)
215+
if email == "" {
216+
return aggregates.User{}, fmt.Errorf("email claim empty")
217+
}
218+
219+
usersApiHost := os.Getenv("USERS_API_HOST")
220+
if usersApiHost == "" {
221+
return aggregates.User{}, fmt.Errorf("users api host empty")
222+
}
223+
224+
resp, err := http.Get(fmt.Sprintf("%s/users/get?email=%s", usersApiHost, email))
225+
if err != nil {
226+
return aggregates.User{}, fmt.Errorf("failed to fetch user id: %v", err)
227+
}
228+
defer func(Body io.ReadCloser) {
229+
_ = Body.Close()
230+
}(resp.Body)
231+
232+
body, bodyErr := io.ReadAll(resp.Body)
233+
if bodyErr != nil {
234+
return aggregates.User{}, fmt.Errorf("failed to read response body: %v", err)
235+
}
236+
237+
var userResult dto.GetUserResult
238+
deserializationErr := json.Unmarshal(body, &userResult)
239+
if deserializationErr != nil {
240+
return aggregates.User{}, fmt.Errorf("failed to deserialize user result: %v", deserializationErr)
241+
}
242+
243+
user, userErr := aggregates.NewUser(userResult.Id, userResult.Username, email, email)
244+
if userErr != nil {
245+
return aggregates.User{}, fmt.Errorf("failed to load user: %v", userErr)
246+
}
247+
248+
return user, nil
249+
}

0 commit comments

Comments
 (0)