Skip to content

Commit

Permalink
Merge pull request #247 from meetnearme/develop
Browse files Browse the repository at this point in the history
[release] Simplify registration / show signups & registrations in user profile
  • Loading branch information
brianfeister authored Dec 20, 2024
2 parents fe58c10 + b58794a commit ce7a6d5
Show file tree
Hide file tree
Showing 38 changed files with 659 additions and 8,021 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: '1.21.x'
- name: npm install
run: |
npm i
- name: Generate Tailwind CSS
# 🚨 Must come before `templ_generate` to ensure CSS hash in
# layout.templ is updated first, before go templates compile
run: |
npm run tailwind:prod
- name: Generate templ code
uses: './.github/actions/templ_generate'
with:
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/deploy-feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: '1.21.x'
- name: npm install
run: |
npm i
- name: Generate Tailwind CSS
# 🚨 Must come before `templ_generate` to ensure CSS hash in
# layout.templ is updated first, before go templates compile
run: |
npm run tailwind:prod
- name: Generate templ code
uses: './.github/actions/templ_generate'
with:
Expand Down Expand Up @@ -78,7 +86,7 @@ jobs:
uses: './.github/actions/generate_cloudflare_locations_file'
- name: Deploy AWS resources via SST
run: |
npm i && npx sst deploy --stage ${{ steps.extract_branch.outputs.branch }}
npx sst deploy --stage ${{ steps.extract_branch.outputs.branch }}
- name: Deploy to Cloudflare Workers with Wrangler
uses: cloudflare/wrangler-action@v3.7.0
env:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: '1.21.x'
- name: npm install
run: |
npm i
- name: Generate Tailwind CSS
# 🚨 Must come before `templ_generate` to ensure CSS hash in
# layout.templ is updated first, before go templates compile
run: |
npm run tailwind:prod
- name: Generate templ code
uses: './.github/actions/templ_generate'
with:
Expand Down
58 changes: 3 additions & 55 deletions API_ENDPOINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ curl -X POST https://devnear.me/api/users \
"role": "standard_user"
}'


```

2. Get User by ID
Expand Down Expand Up @@ -62,7 +62,7 @@ curl -X PUT https://devnear.me/api/users/<:user_id> \

4. Delete User
```bash
curl -X DELETE https://devnear.me/api/users/<:user_id>
curl -X DELETE https://devnear.me/api/users/<:user_id>
```

## Purchasables
Expand Down Expand Up @@ -161,7 +161,7 @@ curl -X POST https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/event-rs
}'
```

2. GET EventRsvp By PK
2. GET EventRsvp By PK
```bash
curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/event-rsvps/6ce1be30-f700-475c-b84a-49af0c73f337/ea49a5f8-e27c-47b0-8237-6f6f380a048c \
-H "Content-Type: application/json"
Expand Down Expand Up @@ -279,57 +279,5 @@ curl -X DELETE https://devnear.me/api/registration-fields/<:event_id> \
```


## Registrations

#### Note eventId comes before userId in url params

1. Create Registration
```bash
curl -X POST https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/99413f71-bb0e-43c9-bc3a-fafc64c5c799 \
-H "Content-Type: application/json" \
-d '{
"responses": [
{"attendeeEmail": "me@meetnear.ne"},
{"tShirtSize": "XL"}
]
}'
```
2. Get Registration by Primary Key
```bash
/api/registrations/{:event_id}/{:user_id}
curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \
-H "Content-Type: application/json"
```

3. Get Registration by EventId
```bash
curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/event/62352e94-b34d-4ee7-a9d1-f1c8e404dec0 \
-H "Content-Type: application/json"
```

4. Get Registration by UserId
```bash
curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/user/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \
-H "Content-Type: application/json"
```

5. Update registration (uses PK)
```bash
curl -X PUT https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \
-H "Content-Type: application/json" \
-d '{
"responses": [
{"attendeeEmail": "newemail@meetnear.ne"},
{"tShirtSize": "L"}
]
}'
```

6. Delete Registration (uses PK)
```bash
curl -X DELETE https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \
-H "Content-Type: application/json"
```



121 changes: 92 additions & 29 deletions functions/gateway/handlers/data_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ type rawEvent struct {
HideCrossPromo *bool `json:"hideCrossPromo,omitempty"`
}

// Create a new struct that includes the createPurchase fields and the Stripe checkout URL
type PurchaseResponse struct {
internal_types.PurchaseInsert
StripeCheckoutURL string `json:"stripe_checkout_url"`
}

func ConvertRawEventToEvent(raw rawEvent, requireId bool) (types.Event, error) {
loc, err := time.LoadLocation(raw.Timezone)
if err != nil {
Expand Down Expand Up @@ -634,7 +640,9 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) {
}

// all purchases are pending and a client passing this status should be overridden
createPurchase.Status = "PENDING"
if createPurchase.Status == "" {
createPurchase.Status = "PENDING"
}

err = json.Unmarshal(body, &createPurchase)
if err != nil {
Expand All @@ -655,12 +663,51 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) {
createdAtString := fmt.Sprintf("%020d", _createdAt) // Pad with zeros to a fixed width of 20 digits

createPurchase.CreatedAtString = createdAtString
referenceId := "event-" + eventId + "-user-" + userId + "-time-" + createPurchase.CreatedAtString

// Create the composite key
compositeKey := fmt.Sprintf("%s_%s_%s", createPurchase.EventID, createPurchase.UserID, createPurchase.CreatedAtString)

// Add the composite key and createdAt to the purchase object
createPurchase.CompositeKey = compositeKey

// Create the purchase record immediately instead of deferring it
purchaseService := dynamodb_service.NewPurchaseService()
purchaseHandler := dynamodb_handlers.NewPurchaseHandler(purchaseService)

// when there are no purchased items, we treat this as an "RSVP" or "INTERESTED" status that shows
// in the users purchase / registration history. The empty PurchasedItems array signals that this
// is an event that does not have `RegistrationFields` or `PurchasableItems`
if len(createPurchase.PurchasedItems) == 0 {
db := transport.GetDB()
log.Printf("createPurchase: %+v", createPurchase)
_, err := purchaseHandler.PurchaseService.InsertPurchase(r.Context(), db, createPurchase)
if err != nil {
transport.SendServerRes(w, []byte("Failed to insert free purchase into database: "+err.Error()), http.StatusInternalServerError, err)
return err
}

// Create the response object
response := PurchaseResponse{
PurchaseInsert: createPurchase,
StripeCheckoutURL: "", // Empty URL for free items
}

// Marshal and send the response
purchaseJSON, err := json.Marshal(response)
if err != nil {
transport.SendServerRes(w, []byte("Failed to marshal purchase response: "+err.Error()), http.StatusInternalServerError, err)
return err
}
transport.SendServerRes(w, purchaseJSON, http.StatusOK, nil)
return nil
}

purchasableService := dynamodb_service.NewPurchasableService()
h := dynamodb_handlers.NewPurchasableHandler(purchasableService)
purchasableHandler := dynamodb_handlers.NewPurchasableHandler(purchasableService)

db := transport.GetDB()
purchasable, err := h.PurchasableService.GetPurchasablesByEventID(r.Context(), db, eventId)
purchasable, err := purchasableHandler.PurchasableService.GetPurchasablesByEventID(r.Context(), db, eventId)
if err != nil {
transport.SendServerRes(w, []byte("Failed to get purchasables for event id: "+eventId+" "+err.Error()), http.StatusInternalServerError, err)
return
Expand All @@ -683,13 +730,11 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) {
}
}

// this boolean gets toggled in the scenario where stripe
// checkout instantiation or other unrelated checkout steps
// AFTER the inventory is officially "held" + optimistically
// decremented
// this boolean gets toggled in the scenario where stripe checkout fails to complete or other
// unrelated checkout failures AFTER the inventory is officially "held" + optimistically decremented
var needsRevert bool

err = h.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, inventoryUpdates, purchasableMap)
err = purchasableHandler.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, inventoryUpdates, purchasableMap)
if err != nil {
transport.SendServerRes(w, []byte("Failed to update inventory: "+err.Error()), http.StatusInternalServerError, err)
return
Expand All @@ -706,13 +751,46 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) {
PurchasableIndex: update.PurchasableIndex,
}
}
revertErr := h.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, revertUpdates, purchasableMap)
revertErr := purchasableHandler.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, revertUpdates, purchasableMap)
if revertErr != nil {
log.Printf("ERR: Failed to revert inventory changes: %v", revertErr)
}
}
}()

// Handle for free item purchases. These still need to track inventory and update the database, though we don't
// need to create a Stripe checkout session
if createPurchase.Total == 0 {
// Skip Stripe checkout for free items
createPurchase.Status = helpers.PurchaseStatus.Registered // Mark as registered immediately since it's free

db := transport.GetDB()
log.Printf("createPurchase: %+v", createPurchase)
_, err := purchaseHandler.PurchaseService.InsertPurchase(r.Context(), db, createPurchase)
if err != nil {
needsRevert = true
transport.SendServerRes(w, []byte("Failed to insert free purchase into database: "+err.Error()), http.StatusInternalServerError, err)
return err
}

// Create the response object
response := PurchaseResponse{
PurchaseInsert: createPurchase,
StripeCheckoutURL: "", // Empty URL for free items
}

// Marshal and send the response
purchaseJSON, err := json.Marshal(response)
if err != nil {
transport.SendServerRes(w, []byte("Failed to marshal purchase response: "+err.Error()), http.StatusInternalServerError, err)
return err
}
log.Printf("purchaseJSON: %s", string(purchaseJSON)) // Conve
transport.SendServerRes(w, purchaseJSON, http.StatusOK, nil)
return nil
}

// Continue with existing Stripe checkout logic for paid items
_, stripePrivKey := services.GetStripeKeyPair()
stripe.Key = stripePrivKey

Expand All @@ -736,10 +814,9 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) {
}
}

referenceId := "event-" + eventId + "-user-" + userId + "-time-" + createPurchase.CreatedAtString
params := &stripe.CheckoutSessionParams{
ClientReferenceID: stripe.String(referenceId), // Store purchase
SuccessURL: stripe.String(os.Getenv("APEX_URL") + "/event/" + eventId + "?checkout=success"),
SuccessURL: stripe.String(os.Getenv("APEX_URL") + "/admin/profile?new_purch_key=" + createPurchase.CompositeKey),
CancelURL: stripe.String(os.Getenv("APEX_URL") + "/event/" + eventId + "?checkout=cancel"),
LineItems: lineItems,
// NOTE: `mode` needs to be "subscription" if there's a subscription / recurring item,
Expand All @@ -764,32 +841,18 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) {
// Now that the checks are in place, we defer the transaction creation in the database
// to respond to the client as quickly as possible
defer func() {
purchaseService := dynamodb_service.NewPurchaseService()
h := dynamodb_handlers.NewPurchaseHandler(purchaseService)
createPurchase.Status = helpers.StripeCheckoutStatus.Pending

// Create the composite key
compositeKey := fmt.Sprintf("%s_%s_%s", createPurchase.EventID, createPurchase.UserID, createPurchase.CreatedAtString)

// Add the composite key and createdAt to the purchase object
createPurchase.CompositeKey = compositeKey
createPurchase.Status = helpers.PurchaseStatus.Pending

log.Printf("db payload `createPurchase`: %+v", createPurchase)
db := transport.GetDB()
_, err := h.PurchaseService.InsertPurchase(r.Context(), db, createPurchase)
_, err := purchaseHandler.PurchaseService.InsertPurchase(r.Context(), db, createPurchase)
if err != nil {
log.Printf("ERR: failed to insert purchase into purchases database for stripe session ID: %+v, err: %+v", stripeCheckoutResult.ID, err)
}
}()

log.Printf("\nstripe result: %+v", stripeCheckoutResult)

// Create a new struct that includes the createPurchase fields and the Stripe checkout URL
type PurchaseResponse struct {
internal_types.PurchaseInsert
StripeCheckoutURL string `json:"stripe_checkout_url"`
}

// Create the response object
response := PurchaseResponse{
PurchaseInsert: createPurchase,
Expand Down Expand Up @@ -895,7 +958,7 @@ func (h *PurchasableWebhookHandler) HandleCheckoutWebhook(w http.ResponseWriter,
return err
}
purchaseUpdate := TransformPurchaseToUpdate(*purchase)
purchaseUpdate.Status = helpers.StripeCheckoutStatus.Settled
purchaseUpdate.Status = helpers.PurchaseStatus.Settled
if checkoutSession.PaymentIntent != nil {
purchaseUpdate.StripeTransactionId = checkoutSession.PaymentIntent.ID
}
Expand Down Expand Up @@ -980,7 +1043,7 @@ func (h *PurchasableWebhookHandler) HandleCheckoutWebhook(w http.ResponseWriter,
return err
}
purchaseUpdate := TransformPurchaseToUpdate(*purchase)
purchaseUpdate.Status = helpers.StripeCheckoutStatus.Canceled
purchaseUpdate.Status = helpers.PurchaseStatus.Canceled

_, err = h.PurchaseService.UpdatePurchase(r.Context(), db, eventID, userID, purchase.CreatedAtString, purchaseUpdate)
if err != nil {
Expand Down
12 changes: 6 additions & 6 deletions functions/gateway/handlers/data_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1171,7 +1171,7 @@ func TestHandleCheckoutWebhook(t *testing.T) {
EventID: eventId,
UserID: userId,
CreatedAtString: createdAt,
Status: helpers.StripeCheckoutStatus.Pending,
Status: helpers.PurchaseStatus.Pending,
PurchasedItems: []internal_types.PurchasedItem{
{
Name: "Test Item",
Expand All @@ -1182,8 +1182,8 @@ func TestHandleCheckoutWebhook(t *testing.T) {
}, nil
},
UpdatePurchaseFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId, createdAt string, update internal_types.PurchaseUpdate) (*internal_types.Purchase, error) {
if update.Status != helpers.StripeCheckoutStatus.Settled {
t.Errorf("expected status %v, got %v", helpers.StripeCheckoutStatus.Settled, update.Status)
if update.Status != helpers.PurchaseStatus.Settled {
t.Errorf("expected status %v, got %v", helpers.PurchaseStatus.Settled, update.Status)
}
return nil, nil
},
Expand Down Expand Up @@ -1357,7 +1357,7 @@ func TestHandleCheckoutWebhook(t *testing.T) {
EventID: eventId,
UserID: userId,
CreatedAtString: createdAt,
Status: helpers.StripeCheckoutStatus.Pending,
Status: helpers.PurchaseStatus.Pending,
PurchasedItems: []internal_types.PurchasedItem{
{
Name: "Test Item",
Expand All @@ -1368,8 +1368,8 @@ func TestHandleCheckoutWebhook(t *testing.T) {
}, nil
},
UpdatePurchaseFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId, createdAt string, update internal_types.PurchaseUpdate) (*internal_types.Purchase, error) {
if update.Status != helpers.StripeCheckoutStatus.Canceled {
t.Errorf("expected status %v, got %v", helpers.StripeCheckoutStatus.Canceled, update.Status)
if update.Status != helpers.PurchaseStatus.Canceled {
t.Errorf("expected status %v, got %v", helpers.PurchaseStatus.Canceled, update.Status)
}
return nil, nil
},
Expand Down
Loading

0 comments on commit ce7a6d5

Please sign in to comment.