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