diff --git a/go.mod b/go.mod index 666bba5..41d8362 100644 --- a/go.mod +++ b/go.mod @@ -9,4 +9,5 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.6.0 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/stripe/stripe-go/v76 v76.12.0 ) diff --git a/go.sum b/go.sum index 793d61d..a2f35a0 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0 h1:DNrExYwvyyI404SxdUCCANAj9 github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0/go.mod h1:SQq4xfIdvf6WYKSDxAJc+xOJdolt+/bc1jnQKMtPMvQ= github.com/brianvoe/gofakeit/v6 v6.23.2 h1:lVde18uhad5wII/f5RMVFLtdQNE0HaGFuBUXmYKk8i8= github.com/brianvoe/gofakeit/v6 v6.23.2/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -10,3 +11,16 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stripe/stripe-go/v76 v76.12.0 h1:TzkkQ1yXEZqO8+WS4Qun/mYKRLFQpPX8bti3VUEoe30= +github.com/stripe/stripe-go/v76 v76.12.0/go.mod h1:rw1MxjlAKKcZ+3FOXgTHgwiOa2ya6CPq6ykpJ0Q6Po4= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grav-admin/user/js/registration/account.vue b/grav-admin/user/js/registration/account.vue index 6c92938..d6f58ac 100644 --- a/grav-admin/user/js/registration/account.vue +++ b/grav-admin/user/js/registration/account.vue @@ -64,7 +64,7 @@ import { ref } from "vue" import Dropdown from "./dropdown.vue" const addresses = ref([]) -const user = ref({}) +const user = ref({country: "USA", title: "Loan Officer"}) const address = ref({}) const locationsId = ref(null) const props = defineProps(['err']) diff --git a/grav-admin/user/js/registration/billing.vue b/grav-admin/user/js/registration/billing.vue new file mode 100644 index 0000000..7b312d9 --- /dev/null +++ b/grav-admin/user/js/registration/billing.vue @@ -0,0 +1,13 @@ + + + diff --git a/grav-admin/user/js/registration/registration.vue b/grav-admin/user/js/registration/registration.vue index 6638bcc..7ec08ac 100644 --- a/grav-admin/user/js/registration/registration.vue +++ b/grav-admin/user/js/registration/registration.vue @@ -2,18 +2,21 @@

Register

+
{% do assets.addJs('jquery', 101) %} {% do assets.addJs('theme://js/jquery.treemenu.js', {group:'bottom'}) %} {% do assets.addJs('theme://js/site.js', {group:'bottom'}) %} diff --git a/migrations/0_29092022_setup_tables.sql b/migrations/0_29092022_setup_tables.sql index a734f07..0eb2d23 100644 --- a/migrations/0_29092022_setup_tables.sql +++ b/migrations/0_29092022_setup_tables.sql @@ -56,6 +56,17 @@ CREATE TABLE user ( FOREIGN KEY (address) REFERENCES address(id) ); +CREATE TABLE subscription ( + id INT AUTO_INCREMENT, + stripe_id VARCHAR(255) DEFAULT '', + user_id INT, + customer_id VARCHAR(255) NOT NULL, + current_period_end INT DEFAULT 0, + current_period_start INT DEFAULT 0, + PRIMARY KEY (`id`), + FOREIGN KEY (user_id) REFERENCES user(id) +); + CREATE TABLE license ( id INT AUTO_INCREMENT, user_id INT NOT NULL, diff --git a/migrations/reset.sql b/migrations/reset.sql index b268e1c..2ebeb7a 100644 --- a/migrations/reset.sql +++ b/migrations/reset.sql @@ -8,6 +8,7 @@ DROP TABLE IF EXISTS borrower; DROP TABLE IF EXISTS estimate; DROP TABLE IF EXISTS loan_type; DROP TABLE IF EXISTS license; +DROP TABLE IF EXISTS subscription; DROP TABLE IF EXISTS user; DROP TABLE IF EXISTS branch; DROP TABLE IF EXISTS address; diff --git a/skouter.go b/skouter.go index d6d8a14..74a848c 100644 --- a/skouter.go +++ b/skouter.go @@ -28,6 +28,10 @@ import ( "github.com/disintegration/gift" "github.com/dustin/go-humanize" "github.com/golang-jwt/jwt/v4" + "github.com/stripe/stripe-go/v76" + "github.com/stripe/stripe-go/v76/customer" + "github.com/stripe/stripe-go/v76/subscription" + // "github.com/stripe/stripe-go/v76/paymentintent" "image" _ "image/jpeg" "image/png" @@ -59,6 +63,16 @@ type Branch struct { Address Address `json:"address"` } +type Subscription struct { + Id int `json:"id"` + UserId int `json:"userId"` + StripeId string `json:"stripeId"` + CustomerId string `json:"customerId"` + Start int `json:"start"` + End int `json:"end"` + ClientSecret string `json:"clientSecret,omitempty"` +} + type User struct { Id int `json:"id"` Email string `json:"email"` @@ -68,12 +82,14 @@ type User struct { Address Address `json:"address"` Branch Branch `json:"branch"` License License `json:"license"` + Sub Subscription `json:"sub"` Status string `json:"status"` Country string `json:"country"` Title string `json:"title"` Verified bool `json:"verified"` Role string `json:"role"` Password string `json:"password,omitempty"` + CustomerId string `json:"customerId"` } type License struct { @@ -1107,6 +1123,7 @@ func queryUser(db *sql.DB, id int) (User, error) { u.country, u.title, coalesce(u.status, ''), + coalesce(u.customer_id, '') u.verified, u.role, u.address, @@ -1128,6 +1145,7 @@ func queryUser(db *sql.DB, id int) (User, error) { &user.Country, &user.Title, &user.Status, + &user.CustomerId, &user.Verified, &user.Role, &user.Address.Id, @@ -1222,6 +1240,60 @@ func queryUsers(db *sql.DB, id int) ([]User, error) { return users, nil } +func querySub(db *sql.DB, id int) (Subscription, error) { + var query string + var err error + var s Subscription + + query = `SELECT + id, + stripe_id, + user_id, + customer_id, + current_period_end, + current_period_start + FROM subscription WHERE id = ? + ` + row := db.QueryRow(query, id) + + err = row.Scan( + &s.Id, + &s.StripeId, + &s.CustomerId, + &s.End, + &s.Start, + ) + + return s, err +} + +func (user *User) querySub(db *sql.DB) error { + var query string + var err error + + query = `SELECT + id, + stripe_id, + user_id, + customer_id, + current_period_end, + current_period_start + FROM subscription WHERE user_id = ? + ` + row := db.QueryRow(query, user.Id) + + err = row.Scan( + &user.Sub.Id, + &user.Sub.StripeId, + &user.Sub.UserId, + &user.Sub.CustomerId, + &user.Sub.End, + &user.Sub.Start, + ) + + return err +} + func (estimate *Estimate) insertResults(db *sql.DB) error { var query string var row *sql.Row @@ -1322,6 +1394,51 @@ func insertUser(db *sql.DB, user User) (int, error) { return id, nil } +// Insert user returning it's ID or any error +func (sub *Subscription) insertSub(db *sql.DB) (error) { + var query string + var err error + + query = `INSERT INTO subscription + ( + stripe_id, + user_id, + customer_id, + current_period_end, + current_period_start + ) + VALUES (?, ?, ?, ?, ?) + ` + + _, err = db.Exec(query, + sub.StripeId, + sub.UserId, + sub.CustomerId, + sub.End, + sub.Start, + ) + + return err +} + + +// Updates a user's stripe customer ID. +func (user *User) updateCustomerId(db *sql.DB, cid string) (error) { + var query string + var err error + + query = `UPDATE user SET + customer_id = ? + WHERE id = ? + ` + _, err = db.Exec(query, + cid, + user.Id, + ) + + return err +} + func updateAddress(address Address, db *sql.DB) error { query := ` UPDATE address @@ -1378,12 +1495,14 @@ func getUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { w.WriteHeader(500) return } + user, err := queryUser(db, claims.Id) if err != nil { w.WriteHeader(422) log.Println(err) return } + json.NewEncoder(w).Encode(user) } @@ -2731,6 +2850,74 @@ func clipLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) { } } +func createCustomer(name string, email string, address Address) ( +stripe.Customer, error) { + + params := &stripe.CustomerParams{ + Email: stripe.String(email), + Name: stripe.String(name), + Address: &stripe.AddressParams{ + City: stripe.String(address.City), + Country: stripe.String(address.Country), + Line1: stripe.String(address.Street), + PostalCode: stripe.String(address.Zip), + State: stripe.String(address.Region), + }, + }; + + result, err := customer.New(params) + return *result, err +} + +func createSubscription(w http.ResponseWriter, db *sql.DB, r *http.Request) { + claims, err := getClaims(r) + user, err := queryUser(db, claims.Id) + if err != nil { + w.WriteHeader(422) + return + } + + var name string = user.FirstName + " " + user.LastName + + c, err := createCustomer(name, user.Email, user.Address) + + if err != nil { + http.Error(w, err.Error(), 422) + return + } + + err = user.updateCustomerId(db, c.ID) + + // Automatically save the payment method to the subscription + // when the first payment is successful. + paymentSettings := &stripe.SubscriptionPaymentSettingsParams{ + SaveDefaultPaymentMethod: stripe.String("on_subscription"), + } + + // Create the subscription. Note we're expanding the Subscription's + // latest invoice and that invoice's payment_intent + // so we can pass it to the front end to confirm the payment + subscriptionParams := &stripe.SubscriptionParams{ + Customer: stripe.String(c.ID), + Items: []*stripe.SubscriptionItemsParams{ + { + Price: stripe.String("price_1OZLK9BPMoXn2pf9kuTAf8rs"), + }, + }, + PaymentSettings: paymentSettings, + PaymentBehavior: stripe.String("default_incomplete"), + } + subscriptionParams.AddExpand("latest_invoice.payment_intent") + s, err := subscription.New(subscriptionParams) + + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + json.NewEncoder(w).Encode(s) +} + func api(w http.ResponseWriter, r *http.Request) { var args []string @@ -3176,6 +3363,7 @@ func dev(args []string) { os.Setenv("DBName", "skouter_dev") os.Setenv("DBUser", "tester") os.Setenv("DBPass", "test123") + stripe.Key = os.Getenv("STRIPE_SECRET_KEY") db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s?multiStatements=true",