/ payment.go
payment.go
1 package main 2 3 import ( 4 "encoding/json" 5 "log" 6 "strconv" 7 "time" 8 9 "github.com/google/uuid" 10 "github.com/maxence-charriere/go-app/v10/pkg/app" 11 shell "github.com/stateless-minds/go-ipfs-api" 12 ) 13 14 const dbTransaction = "transaction" 15 16 // payment is a component that holds cyber-gubi. A component is a 17 // customizable, independent, and reusable UI element. It is created by 18 // embedding app.Compo into a struct. 19 type payment struct { 20 app.Compo 21 sh *shell.Shell 22 loggedIn bool 23 userID string 24 wallet Wallet 25 wallets []Wallet 26 productsIndex []int 27 servicesIndex []int 28 products []ProductService 29 services []ProductService 30 activeTab string 31 } 32 33 type Subscription struct { 34 ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"` // Unique identifier for the transaction 35 PlanID string `mapstructure:"plan_id" json:"plan_id" validate:"uuid_rfc4122"` // Plan id 36 UserID string `mapstructure:"user_id" json:"user_id" validate:"uuid_rfc4122"` // User id 37 Price int `mapstructure:"price" json:"price" validate:"uuid_rfc4122"` // Price 38 StartDate time.Time `mapstructure:"start_date" json:"start_date" validate:"uuid_rfc4122"` // Start date of subscription 39 EndDate time.Time `mapstructure:"end_date" json:"end_date" validate:"uuid_rfc4122"` // End date of subscription 40 } 41 42 type Transaction struct { 43 ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"` // Unique identifier for the transaction 44 SenderID string `mapstructure:"sender_id" json:"sender_id" validate:"uuid_rfc4122"` // Sender user id 45 ReceiverID string `mapstructure:"receiver_id" json:"receiver_id" validate:"uuid_rfc4122"` // Recipient user id 46 ProductsServices []ProductService 47 TotalCost int `mapstructure:"total_cost" json:"total_cost" validate:"uuid_rfc4122"` // Total cost of transaction 48 Timestamp time.Time `mapstructure:"timestamp" json:"timestamp" validate:"uuid_rfc4122"` // Timestamp of the transaction 49 Date string `mapstructure:"date" json:"date" validate:"uuid_rfc4122"` // Date of the transaction in the format YY/MM 50 Processed bool `mapstructure:"processed" json:"processed" validate:"uuid_rfc4122"` // Flag if it was already processed by inflation indexer 51 } 52 53 type ProductService struct { 54 ID string `mapstructure:"product_id" json:"product_id" validate:"uuid_rfc4122"` // Unique identifier for the product 55 Name string `mapstructure:"name" json:"name" validate:"uuid_rfc4122"` 56 Price int `mapstructure:"price" json:"price" validate:"uuid_rfc4122"` 57 Amount int `mapstructure:"amount" json:"amount" validate:"uuid_rfc4122"` 58 } 59 60 func (p *payment) OnMount(ctx app.Context) { 61 sh := shell.NewShell("localhost:5001") 62 p.sh = sh 63 64 // set default number of product inputs 65 p.productsIndex = []int{1} 66 p.products = make([]ProductService, 1) 67 // set default number of service inputs 68 p.servicesIndex = []int{1} 69 p.services = make([]ProductService, 1) 70 p.activeTab = "product" 71 72 ctx.GetState("loggedIn", &p.loggedIn) 73 if !p.loggedIn { 74 ctx.Navigate("/auth") 75 } 76 77 ctx.GetState("userID", &p.userID) 78 ctx.GetState("balance", &p.wallet) 79 80 p.getBalances(ctx) 81 } 82 83 func (p *payment) getBalance(userID string) (balance Wallet, err error) { 84 b, err := p.sh.OrbitDocsQuery(dbWallet, "_id", userID) 85 if err != nil { 86 return Wallet{}, err 87 } 88 89 if len(b) == 0 { 90 return Wallet{}, err 91 } 92 93 wallets := []Wallet{} 94 95 err = json.Unmarshal(b, &wallets) // Unmarshal the byte slice directly 96 if err != nil { 97 return Wallet{}, err 98 } 99 100 return wallets[0], nil 101 } 102 103 func removeSelfFromUserResults(wallets []Wallet, userID string) []Wallet { 104 for i, ub := range wallets { 105 if ub.ID == userID { 106 return append(wallets[:i], wallets[i+1:]...) 107 } 108 } 109 return wallets 110 } 111 112 func (p *payment) getBalances(ctx app.Context) { 113 ctx.Async(func() { 114 b, err := p.sh.OrbitDocsQuery(dbWallet, "all", "") 115 if err != nil { 116 log.Fatal(err) 117 } 118 119 if len(b) == 0 { 120 log.Fatal(err) 121 } 122 123 wallets := []Wallet{} 124 125 err = json.Unmarshal(b, &wallets) // Unmarshal the byte slice directly 126 if err != nil { 127 log.Fatal(err) 128 } 129 130 wallets = removeSelfFromUserResults(wallets, p.userID) 131 132 ctx.Dispatch(func(ctx app.Context) { 133 p.wallets = wallets 134 }) 135 136 }) 137 } 138 139 func (p *payment) updateBalance(userID string, balance, income int, date string) error { 140 wallet := Wallet{ 141 ID: userID, 142 Balance: balance, 143 Income: income, 144 LastReceived: date, 145 } 146 147 walletJSON, err := json.Marshal(wallet) 148 if err != nil { 149 return err 150 } 151 152 err = p.sh.OrbitDocsPut(dbWallet, walletJSON) 153 if err != nil { 154 return err 155 } 156 157 return nil 158 } 159 160 func (p *payment) storeTransaction(transaction Transaction) error { 161 transactionJSON, err := json.Marshal(transaction) 162 if err != nil { 163 return err 164 } 165 166 err = p.sh.OrbitDocsPut(dbTransaction, transactionJSON) 167 if err != nil { 168 return err 169 } 170 171 return nil 172 } 173 174 func (p *payment) showProduct(ctx app.Context, e app.Event) { 175 e.PreventDefault() 176 p.activeTab = "product" 177 elems := app.Window().Get("document").Call("querySelectorAll", ".service") 178 for i := 0; i < elems.Length(); i++ { 179 elems.Index(i).Call("removeAttribute", "required") 180 } 181 app.Window().GetElementByID("tab-service").Get("classList").Call("remove", "tab-active") 182 ctx.JSSrc().Get("classList").Call("add", "tab-active") 183 // Hide Service Tab and show Product Tab 184 app.Window().GetElementByID("product-tab").Call("setAttribute", "style", "display: block") 185 app.Window().GetElementByID("service-tab").Call("setAttribute", "style", "display: none") 186 } 187 188 func (p *payment) showService(ctx app.Context, e app.Event) { 189 e.PreventDefault() 190 p.activeTab = "service" 191 elemsProduct := app.Window().Get("document").Call("querySelectorAll", ".product") 192 for i := 0; i < elemsProduct.Length(); i++ { 193 elemsProduct.Index(i).Call("removeAttribute", "required") 194 } 195 app.Window().GetElementByID("tab-product").Get("classList").Call("remove", "tab-active") 196 ctx.JSSrc().Get("classList").Call("add", "tab-active") 197 // Hide Product Tab and show Service Tab 198 app.Window().GetElementByID("product-tab").Call("setAttribute", "style", "display: none; ") 199 elems := app.Window().Get("document").Call("querySelectorAll", ".service") 200 for i := 0; i < elems.Length(); i++ { 201 elems.Index(i).Call("setAttribute", "required", true) 202 } 203 app.Window().GetElementByID("service-tab").Call("setAttribute", "style", "display: block") 204 } 205 206 func (p *payment) addProduct(ctx app.Context, e app.Event) { 207 e.PreventDefault() 208 209 p.products = append(p.products, ProductService{}) 210 p.productsIndex = append(p.productsIndex, len(p.productsIndex)+1) 211 } 212 213 func (p *payment) removeProduct(ctx app.Context, e app.Event) { 214 e.PreventDefault() 215 p.productsIndex = p.productsIndex[:len(p.productsIndex)-1] 216 p.products = p.products[:len(p.products)-1] 217 } 218 219 func (p *payment) addService(ctx app.Context, e app.Event) { 220 e.PreventDefault() 221 222 p.services = append(p.services, ProductService{}) 223 p.servicesIndex = append(p.servicesIndex, len(p.servicesIndex)+1) 224 } 225 226 func (p *payment) removeService(ctx app.Context, e app.Event) { 227 e.PreventDefault() 228 p.servicesIndex = p.servicesIndex[:len(p.servicesIndex)-1] 229 p.services = p.services[:len(p.services)-1] 230 } 231 232 func (p *payment) doPayment(ctx app.Context, e app.Event) { 233 e.PreventDefault() 234 235 valid := app.Window().GetElementByID("pay-form").Call("reportValidity").Bool() 236 if valid { 237 tabActive := app.Window().Get("document").Call("getElementsByClassName", "tab-active").Index(0).Get("value").String() 238 receiverID := app.Window().GetElementByID("receiver-id").Get("value").String() 239 transaction := Transaction{} 240 transaction.ID = uuid.NewString() 241 transaction.SenderID = p.userID 242 transaction.ReceiverID = receiverID 243 transaction.Timestamp = time.Now() 244 transaction.Date = strconv.Itoa(time.Now().Year()) + "/" + strconv.Itoa(int(time.Now().Month())) 245 if tabActive == "product" { 246 for i, pr := range p.products { 247 p.products[i].ID = uuid.NewString() 248 p.products[i].Price = pr.Price * 100 249 } 250 transaction.ProductsServices = p.products 251 } else { 252 for i, sr := range p.services { 253 p.services[i].ID = uuid.NewString() 254 p.services[i].Price = sr.Price * 100 255 } 256 transaction.ProductsServices = p.services 257 } 258 259 var totalCost int 260 261 for _, ps := range transaction.ProductsServices { 262 totalCost += ps.Price * ps.Amount 263 } 264 265 transaction.TotalCost = totalCost 266 267 if p.wallet.Balance-totalCost < 0 { 268 ctx.Notifications().New(app.Notification{ 269 Title: "Error", 270 Body: "Not enough funds.", 271 }) 272 return 273 } 274 // update sender balance 275 err := p.updateBalance(p.userID, p.wallet.Balance-totalCost, p.wallet.Income, p.wallet.LastReceived) 276 if err != nil { 277 log.Fatal(err) 278 } 279 // get receiver balance 280 receiverBalance, err := p.getBalance(transaction.ReceiverID) 281 if err != nil { 282 log.Fatal(err) 283 } 284 // update receiver balance 285 err = p.updateBalance(transaction.ReceiverID, receiverBalance.Balance+totalCost, receiverBalance.Income, receiverBalance.LastReceived) 286 if err != nil { 287 // rollback sender balance 288 err := p.updateBalance(p.userID, p.wallet.Balance+totalCost, p.wallet.Income, p.wallet.LastReceived) 289 if err != nil { 290 log.Fatal(err) 291 } 292 return 293 } 294 // store transaction 295 err = p.storeTransaction(transaction) 296 if err != nil { 297 // rollback sender balance 298 err = p.updateBalance(p.userID, p.wallet.Balance+totalCost, p.wallet.Income, p.wallet.LastReceived) 299 if err != nil { 300 log.Fatal(err) 301 } 302 // rollback receiver balance 303 err = p.updateBalance(transaction.ReceiverID, receiverBalance.Balance-totalCost, receiverBalance.Income, receiverBalance.LastReceived) 304 if err != nil { 305 log.Fatal(err) 306 } 307 return 308 } 309 310 p.wallet.Balance = p.wallet.Balance - totalCost 311 ctx.Update() 312 313 ctx.Notifications().New(app.Notification{ 314 Title: "Success", 315 Body: "Payment successful!", 316 }) 317 } 318 } 319 320 // The Render method is where the component appearance is defined. Here, a 321 // payment form is displayed. 322 func (p *payment) Render() app.UI { 323 return app.Div().Class("container").Body( 324 app.Div().Class("mobile").Body( 325 app.Div().Class("header").Body( 326 newNav(), 327 app.Div().Class("header-summary").Body( 328 app.Span().Class("logo").Text("cyber-gubi"), 329 app.Div().Class("summary-text").Body( 330 app.Span().Text("Balance"), 331 ), 332 app.Div().Class("summary-balance").Body( 333 app.Span().Text(strconv.Itoa(p.wallet.Balance/100)+" GUBI"), 334 ), 335 ), 336 ), 337 app.Div().ID("content").Body( 338 app.Div().Class("card").Body( 339 app.Div().Class("upper-row").Body( 340 app.Div().Class("card-item").Body( 341 app.Span().Class("span-header").Text("Make Payment"), 342 app.Form().ID("pay-form").Body( 343 app.Label().For("receiver-id").Text("Receiver ID:"), 344 app.Select().ID("receiver-id").Name("receiver-id").Body( 345 app.Range(p.wallets).Slice(func(i int) app.UI { 346 return app.Option().Value(p.wallets[i].ID).Text(p.wallets[i].ID) 347 }), 348 ), 349 // Tab Navigation 350 app.Div(). 351 Class("tabs"). 352 Body( 353 app.Button(). 354 ID("tab-product"). 355 Class("tab-button"). 356 Class("tab-active"). 357 Text("Product"). 358 Value("product"). 359 OnClick(p.showProduct), 360 app.Button(). 361 ID("tab-service"). 362 Class("tab-button"). 363 Text("Service"). 364 Value("service"). 365 OnClick(p.showService), 366 ), 367 368 // Product Tab Content 369 app.Div(). 370 ID("product-tab"). 371 Class("tab-content"). 372 Body( 373 app.Range(p.productsIndex).Slice(func(i int) app.UI { 374 return app.Div().Body( 375 app.Input().ID("product-name-"+strconv.Itoa(i)).Class("product").Type("text").Name("product-name").Placeholder("Product name").Required(true).OnChange(p.ValueTo(&p.products[i].Name)), 376 app.Input().ID("product-price-"+strconv.Itoa(i)).Class("product").Type("number").Min(1).Name("product-price").Placeholder("Single price").Required(true).OnChange(p.ValueTo(&p.products[i].Price)), 377 app.Input().ID("product-amount-"+strconv.Itoa(i)).Class("product").Type("number").Min(1).Name("product-amount").Step(1).Placeholder("Number of products").Required(true).OnChange(p.ValueTo(&p.products[i].Amount)), 378 ) 379 }), 380 ), 381 // Service Tab Content 382 app.Div(). 383 ID("service-tab"). 384 Class("tab-content"). 385 Body( 386 app.Range(p.servicesIndex).Slice(func(i int) app.UI { 387 return app.Div().Body( 388 app.Input().ID("service-name").Class("service").Type("text").Name("service-name").Placeholder("Service name").OnChange(p.ValueTo(&p.services[i].Name)), 389 app.Input().ID("service-price").Class("service").Type("number").Min(1).Name("service-price").Placeholder("Price per hour").OnChange(p.ValueTo(&p.services[i].Price)), 390 app.Input().ID("service-amount").Class("service").Type("number").Min(1).Name("service-amount").Step(1).Placeholder("Number of hours").OnChange(p.ValueTo(&p.services[i].Amount)), 391 ) 392 }), 393 ).Hidden(true), 394 app.If(p.activeTab == "product", func() app.UI { 395 return app.Div().Class("menu-btn menu-add-item").Body( 396 app.Button().Class("submit").Text("+").OnClick(p.addProduct), 397 app.If(len(p.productsIndex) > 1, func() app.UI { 398 return app.Button().Class("submit").Text("-").OnClick(p.removeProduct) 399 }), 400 ) 401 }).Else(func() app.UI { 402 return app.Div().Class("menu-btn menu-add-item").Body( 403 app.Button().Class("submit").Text("+").OnClick(p.addService), 404 app.If(len(p.servicesIndex) > 1, func() app.UI { 405 return app.Button().Class("submit").Text("-").OnClick(p.removeService) 406 }), 407 ) 408 }), 409 app.Div().Class("drawer drawer-pay").Body( 410 app.Div().Class("menu-btn").Body( 411 app.Button().Class("submit").Type("submit").Text("Pay").OnClick(p.doPayment), 412 ), 413 ), 414 ), 415 ), 416 ), 417 ), 418 ), 419 ), 420 ) 421 }