Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release] Simplify registration / show signups & registrations in user profile #247

Merged
merged 21 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0790e04
Remove `eventRsvps` and `registrations` in favor of using `purchases`…
brianfeister Dec 18, 2024
0e472ce
Finish removal of `event_rsvps` and `registrations` tables. Migrate e…
brianfeister Dec 20, 2024
f116c42
Merge branch 'develop' into feature/simplify-registrations-rsvps
brianfeister Dec 20, 2024
469ce80
Fix incorrect test env var set via cloudflare, causing the same port …
brianfeister Dec 20, 2024
b203fce
properly prefix cloudflare bound address in test run with protocol
brianfeister Dec 20, 2024
0528f87
cleanup resolved TODOs
brianfeister Dec 20, 2024
d324e11
cleanup more resolved TODOs
brianfeister Dec 20, 2024
a46ec93
Add missing postcss config to resolve deployments not having a CSS fi…
brianfeister Dec 20, 2024
a043308
Merge branch 'develop' into feature/simplify-registrations-rsvps
brianfeister Dec 20, 2024
4f983ef
remove upstream prod hotfix CSS addition to test deployment on dev
brianfeister Dec 20, 2024
efbd74f
Refactoring postcss hash watching function to handle for production b…
brianfeister Dec 20, 2024
d753178
add missing node env var to trigger production tailwind
brianfeister Dec 20, 2024
2315e5b
create `styles.css` in tailwind production generation if it doesn't e…
brianfeister Dec 20, 2024
1ceb2be
add debugging steps to CSS hashing / generation
brianfeister Dec 20, 2024
3bac645
move breakpoint to last position, drop `npm run tailwind:prod`
brianfeister Dec 20, 2024
2bbbdd0
fix `tailwind:prod` + `templ_generate` order of operations
brianfeister Dec 20, 2024
5ed0c01
implement `scrollTo` for post-purchase, improve UI/UX in checkout fl…
brianfeister Dec 20, 2024
212a985
make order of operations dependency explicit + make changes in `deplo…
brianfeister Dec 20, 2024
ee68eb1
Merge pull request #246 from meetnearme/feature/simplify-registration…
brianfeister Dec 20, 2024
9851260
fix github actions deployment workflows
brianfeister Dec 20, 2024
b58794a
`needs` keyword only works for jobs, removing
brianfeister Dec 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading