-
Notifications
You must be signed in to change notification settings - Fork 34
feat(eai): add handleRefundAndRemoveFromCalendar function for processing refunds and updating calendar events #477
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
feat(eai): add handleRefundAndRemoveFromCalendar function for processing refunds and updating calendar events #477
Conversation
…ing refunds and updating calendar events
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughA new event-driven workflow is introduced to handle refunds by removing refunded users from Google Calendar events associated with live products. This involves defining a new refund event schema, emitting the event after a refund is processed, and implementing an Inngest function to process the event and remove the user from the calendar. Supporting configuration and type definitions are also updated. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant System
participant Inngest
participant GoogleCalendar
User->>System: Initiates refund
System->>System: Process refund logic
System->>Inngest: Emit REFUND_PROCESSED_EVENT (with merchantChargeId)
Inngest->>System: Trigger handleRefundAndRemoveFromCalendar
System->>System: Lookup charge, purchase, product, user, event
alt Product is live and event found
System->>GoogleCalendar: Remove user from event (by email)
GoogleCalendar-->>System: Confirmation
else Product not live or event missing
System->>System: Skip removal
end
System->>Inngest: Report outcome (success, error, or skipped)
Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
Scope: all 3 workspace projects This error happened while installing a direct dependency of /tmp/eslint/packages/config/eslint-config Packages found in the workspace: ✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (4)
packages/core/src/inngest/index.ts (1)
23-26
: Prefer splitting value vs type imports for consistency & tree-shakingThroughout the file we import runtime constants and purely‐compile-time types in separate statements, e.g.:
import { NEW_PURCHASE_CREATED_EVENT, NewPurchaseCreated, } from './commerce/event-new-purchase-created'For the newly added refund event you again mix the two in one import. Consider:
-import { - REFUND_PROCESSED_EVENT, - RefundProcessed, -} from './commerce/event-refund-processed' +import { REFUND_PROCESSED_EVENT } from './commerce/event-refund-processed' +import type { RefundProcessed } from './commerce/event-refund-processed'Benefits
• keeps the pattern consistent with the earlierimport type
blocks in the file
• guarantees that theRefundProcessed
symbol is fully erased from JS output, yielding slightly leaner bundles.apps/epicdev-ai/src/inngest/functions/calendar-sync.ts (3)
14-15
: Type safety TODO can be resolved right away
GetFunctionInput
is already imported. You can give the handler strong typing without waiting:-// TODO: Define a more specific type for the event data if available -// type RefundProcessedEvent = GetFunctionInput<typeof REFUND_PROCESSED_EVENT>['data'] +type RefundProcessedEvent = GetFunctionInput<typeof REFUND_PROCESSED_EVENT>['data']and then
-async ({ event, step, logger }) => { +async ( + { event, step, logger }: { event: { name: typeof REFUND_PROCESSED_EVENT; data: RefundProcessedEvent } } , +) => {This prevents accidental
any
leaks and gives autocomplete formerchantChargeId
.
480-486
: Log message typo may confuse debugging
getEvent.getProduct
does not exist; the message should simply referencegetEvent
.-`Event resource ${eventResourceId} not found or failed validation via getEvent.getProduct`, +`Event resource ${eventResourceId} not found or failed validation via getEvent`,Tiny, but log hygiene matters when you’re triaging incidents.
293-307
: Adapter method presence check can be simplifiedYou test each adapter property individually, but if any are missing you throw the same error.
A helper promotes DRY and makes future additions safer:const required = [ 'getMerchantCharge', 'getPurchaseForStripeCharge', 'getUser', 'getProduct', 'getContentResource', ] as const for (const fn of required) { if (typeof (courseBuilderAdapter as any)[fn] !== 'function') { logger.error(`CourseBuilderAdapter missing method ${fn}`) throw new NonRetriableError('Adapter incomplete for calendar removal') } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting
📒 Files selected for processing (6)
apps/epicdev-ai/.env.development
(1 hunks)apps/epicdev-ai/src/inngest/functions/calendar-sync.ts
(2 hunks)apps/epicdev-ai/src/inngest/inngest.config.ts
(2 hunks)packages/core/src/inngest/commerce/event-refund-processed.ts
(1 hunks)packages/core/src/inngest/index.ts
(2 hunks)packages/core/src/lib/actions/process-refund.ts
(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/core/src/lib/actions/process-refund.ts (1)
packages/core/src/inngest/commerce/event-refund-processed.ts (1)
REFUND_PROCESSED_EVENT
(3-3)
apps/epicdev-ai/src/inngest/inngest.config.ts (1)
apps/epicdev-ai/src/inngest/functions/calendar-sync.ts (1)
handleRefundAndRemoveFromCalendar
(273-547)
packages/core/src/inngest/index.ts (1)
packages/core/src/inngest/commerce/event-refund-processed.ts (2)
REFUND_PROCESSED_EVENT
(3-3)RefundProcessed
(5-8)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: format
- GitHub Check: build
🔇 Additional comments (7)
packages/core/src/inngest/commerce/event-refund-processed.ts (1)
1-14
: Well-structured event schema with proper validation.This is a clean implementation of the refund processed event schema. The use of Zod for validation ensures type safety and runtime validation for the event data.
apps/epicdev-ai/.env.development (1)
6-6
: Support email updated to match product branding.The support email has been updated from "[email protected]" to "[email protected]", which aligns with the product branding.
packages/core/src/lib/actions/process-refund.ts (2)
1-1
: LGTM! Appropriate import for the refund event.The import for the refund processed event constant is correctly added.
36-45
: Well-implemented event emission with proper error handling.The event emission is correctly wrapped in a try-catch block, ensuring that any errors during event sending won't affect the main refund flow. The event includes the necessary
merchantChargeId
data, which will be used by the calendar sync function to locate the relevant user and product.apps/epicdev-ai/src/inngest/inngest.config.ts (2)
17-20
: Clean import of the new calendar function.The import has been properly updated to include the new
handleRefundAndRemoveFromCalendar
function.
59-59
: Correct registration of the new Inngest function.The
handleRefundAndRemoveFromCalendar
function has been properly added to the Inngest configuration, ensuring it will be triggered when refund events occur.packages/core/src/inngest/index.ts (1)
91-91
: Remember to re-export the new event type if external packages rely on itYou added the mapping in
CourseBuilderCoreEvents
, 👍.
If downstream packages cast/extend this union they may need to re-exportRefundProcessed
alongside the constant so consumers can narrow on the type. Double-check@coursebuilder/core
barrel exports.
'find-event-resource-id-for-product', | ||
async () => { | ||
const resources = purchasedProduct.resources | ||
|
||
if (!Array.isArray(resources)) { | ||
logger.warn( | ||
`Product ${purchasedProductId} does not have a 'resources' array or it's not an array. Cannot find event resource.`, | ||
) | ||
return null | ||
} | ||
|
||
// Use .find() as seen in post-event-purchase.ts | ||
const foundEventResource = resources.find( | ||
(item) => item.resource?.type === 'event', | ||
)?.resource | ||
|
||
return foundEventResource?.id || null | ||
}, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Potential null-access on nested resource objects
resources.find((item) => item.resource?.type === 'event')
assumes every resources
element has a resource
property.
If an element is null
/undefined
, the optional-chaining won’t guard the earlier item.resource
access itself and could still raise if item
is nullish.
A slightly safer variant:
const found = resources.find(
(r): r is { resource: { type: string; id: string } } =>
!!r?.resource && r.resource.type === 'event',
)
or use a for … of
loop with explicit checks.
🤖 Prompt for AI Agents
In apps/epicdev-ai/src/inngest/functions/calendar-sync.ts between lines 418 and
436, the code uses resources.find with a callback accessing item.resource?.type
but does not check if item itself is null or undefined, which can cause runtime
errors. To fix this, update the find callback to first check that the item is
not nullish before accessing its resource property, either by using a type guard
with a condition like !!item?.resource && item.resource.type === 'event' or by
replacing the find with a for...of loop that explicitly checks each item and its
resource property before accessing type.
// Step 7: Remove User from Google Calendar Event | ||
try { | ||
await step.run('remove-user-from-google-event', async () => { | ||
await removeUserFromGoogleCalendarEvent(calendarId, user.email) | ||
}) | ||
logger.info( | ||
`Successfully removed user ${user.email} from calendar event ${calendarId}`, | ||
) | ||
return { | ||
outcome: 'success', | ||
userId, | ||
email: user.email, | ||
calendarId, | ||
eventResourceId, | ||
} | ||
} catch (error: any) { | ||
logger.error( | ||
`Failed to remove user ${user.email} from calendar event ${calendarId}: ${error.message}`, | ||
{ error }, | ||
) | ||
// Depending on the error, you might want to retry. | ||
// For now, assume most GCal API errors might be retriable if not handled by removeUserFromGoogleCalendarEvent itself. | ||
throw error | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Gracefully handle “attendee already removed / not found” errors
removeUserFromGoogleCalendarEvent
will throw on any non-2xx Google error.
For refunds it’s common that the user was never on the guest list (manual removal, previous retries, etc.).
Consider inspecting the error code (usually 404
or Google’s status === 'failedPrecondition'
) and returning a NonRetriableError
rather than re-throwing, to avoid noisy retries that can never succeed.
-} catch (error: any) {
+} catch (error: any) {
+ if (isNotFoundOrAlreadyRemoved(error)) {
+ logger.warn(`User ${user.email} not present on event ${calendarId}, skipping retry`)
+ return { outcome: 'skipped', reason: 'attendee absent', calendarId, userId }
+ }
logger.error(
(Implement isNotFoundOrAlreadyRemoved
to parse Google error responses.)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Step 7: Remove User from Google Calendar Event | |
try { | |
await step.run('remove-user-from-google-event', async () => { | |
await removeUserFromGoogleCalendarEvent(calendarId, user.email) | |
}) | |
logger.info( | |
`Successfully removed user ${user.email} from calendar event ${calendarId}`, | |
) | |
return { | |
outcome: 'success', | |
userId, | |
email: user.email, | |
calendarId, | |
eventResourceId, | |
} | |
} catch (error: any) { | |
logger.error( | |
`Failed to remove user ${user.email} from calendar event ${calendarId}: ${error.message}`, | |
{ error }, | |
) | |
// Depending on the error, you might want to retry. | |
// For now, assume most GCal API errors might be retriable if not handled by removeUserFromGoogleCalendarEvent itself. | |
throw error | |
} | |
// Step 7: Remove User from Google Calendar Event | |
try { | |
await step.run('remove-user-from-google-event', async () => { | |
await removeUserFromGoogleCalendarEvent(calendarId, user.email) | |
}) | |
logger.info( | |
`Successfully removed user ${user.email} from calendar event ${calendarId}`, | |
) | |
return { | |
outcome: 'success', | |
userId, | |
email: user.email, | |
calendarId, | |
eventResourceId, | |
} | |
} catch (error: any) { | |
if (isNotFoundOrAlreadyRemoved(error)) { | |
logger.warn( | |
`User ${user.email} not present on event ${calendarId}, skipping retry` | |
) | |
return { | |
outcome: 'skipped', | |
reason: 'attendee absent', | |
userId, | |
calendarId, | |
} | |
} | |
logger.error( | |
`Failed to remove user ${user.email} from calendar event ${calendarId}: ${error.message}`, | |
{ error }, | |
) | |
// Depending on the error, you might want to retry. | |
// For now, assume most GCal API errors might be retriable if not handled by removeUserFromGoogleCalendarEvent itself. | |
throw error | |
} |
🤖 Prompt for AI Agents
In apps/epicdev-ai/src/inngest/functions/calendar-sync.ts around lines 522 to
545, the catch block for removeUserFromGoogleCalendarEvent currently re-throws
all errors, causing unnecessary retries for cases where the user is already
removed or not found. Implement a helper function isNotFoundOrAlreadyRemoved to
detect these specific Google API errors (e.g., 404 status or
failedPrecondition). In the catch block, check the error with this helper and if
it matches, throw a NonRetriableError instead of the original error to prevent
retries; otherwise, re-throw as usual.
Summary by CodeRabbit