Asynchronous Event Processing
Problem statement
Stripe handles millions of payments per day, amounting to billions of dollars of transactions. As you can imagine, this has a huge compute load as a consequence. Because of that, Stripe has been optimized to handle a lot of asynchronous events (think API calls). This allows us to distribute the load over time and leaves our architecture reliable against traffic spikes.
For this question, you will be building a small-scale system that can receive these events, parse and aggregate them into useful states and data. Events with the same event_id should not be processed again.
The question is broken into 4 parts, and we recommend you work on them in order one at a time.
Input (STDIN) For Custom Testing:
- 1 line with
n, the number of events nlines, each containing a single csv shaped string representing one event
Output:
- One line per aggregated payment, with comma-separated values (no header row)
- Column order:
payment_id,merchant_id,state,amount_authorized,amount_captured,last_event_at - Numeric columns default to
0when no value has been accumulated (e.g.payment_1,merchant_1,created,0,0,0) - Output payments in the order they were created (i.e., the order their
createevent was first processed)
Part 1 - Aggregating Payment States
You will be completing the function:
process_events(events: list[str]) -> list[str]
The function receives a list of events that need to be processed, in the order they were received. Each event has the following properties:
event_id: string:unique id of the eventevent_time: intevent_type: string: payment | refund | merchant_updatepayment_id: string:payment id of the eventpayment_event_type: string: create| authorize | capturemerchant_id: string:merchant id associated to that payment eventamount: int
If an input property is null or empty it will be represented with a -. The output should not include - and leave no characters if the property is not present.
Aggregate all payment events in order to keep the most up-to-date information of a payment, having the correct state for it. For this part, you will only be working with payment event types.
A payment will only ever be attached to a single merchant.
For context, a basic payment has 2 stages: authorization and capture. Authorization for a card means that you, the buyer, have authorized the payment for your card, but no money has moved yet. A merchant (a.k.a. seller) can capture the payment later, and that is when the money actually moves.
A payment is considered partially_captured if a capture has been made, but the amount_captured < amount_authorized. A payment is considered successful after the full amount authorized has been captured, basically when amount_authorized == amount_captured. You can assume that the amount captured will never exceed the amount authorized.
The expected transition states between payment events:
| Current Payment State | Valid Events |
|---|---|
| Payment non-existent | create |
created | authorize |
authorized | authorize, capture |
partially_captured | capture |
successful | - |
For this part, you can assume that the events will always come in the correct order and multiple authorize and capture events can exist for the same payment.
After all events have been processed, we want the following string for each payment_id:
payment_id,merchant_id,state,amount_authorized,amount_captured,last_event_at
A payment event comes in the shape:
event_id,event_time,event_type,payment_id,payment_event_type,merchant_id,amount
merchant_idis only present oncreateeventsamountis only present onauthorizeandcaptureevents
Part 2 - Ignoring out-of-order events
Great, our system is processing millions of events. But now we have noticed a new issue: we are getting some events out-of-order of what a valid payment state should accept. For example, a payment that is in partially_captured should not accept an authorize event.
We need to make sure that only valid events are processed. events that are invalid should simply be ignored.
| Current Payment State | Valid Events |
|---|---|
| Payment non-existent | create |
created | authorize |
authorized | authorize, capture |
partially_captured | capture |
successful | - |
Part 3 - Processing refunds
Now that our payments events can handle the correct and incorrect states, we want to also support refunds.
Refunds come in via a separate event type and come in only with the payment_id as the event_data. After receiving a refund event only the state and the last_event_at properties change.
event_id,event_time,event_type,payment_id,payment_event_type,merchant_id,amount
Now the new acceptable state transitions are:
| Payment State | Valid Events |
|---|---|
| Payment non-existent | create |
created | authorize |
authorized | authorize, capture |
partially_captured | capture |
successful | refund |
refunded | - |
Part 4 - Receiving merchant events
In addition to payment and refund events, your system must now process merchant risk updates.
Each merchant has a risk score represented by an integer from 0 through 100, inclusive. A higher score indicates greater risk.
A merchant update uses the same CSV column order as the previous parts:
event_id,event_time,event_type,payment_id,payment_event_type,merchant_id,amount
For a merchant update:
event_typeismerchant_update.merchant_ididentifies the merchant.amountcontains the merchant's risk score.payment_idandpayment_event_typeare empty (-).
For example:
evt_1,0,merchant_update,-,-,merchant_1,95
In order to process the payments correctly, we want to flag high risk merchants, and block all new authorizations from being processed.
When a valid merchant_update event has a risk score greater than or equal to 80, the merchant becomes blocked.
Blocking takes effect immediately:
- Ignore every
authorizeevent received for that merchant after the merchant becomes blocked. - This applies whether the payment was created before or after the merchant became blocked.
- Continue to accept
createevents for the merchant. - Continue to process valid
captureandrefundevents. This allows payments authorized before the merchant was blocked to complete their lifecycle. - A merchant update does not directly change any existing payment's state, amounts, or
last_event_at.
If an authorization is ignored for a payment in the created state, that payment remains created. Payments that were already authorized or partially captured keep their current state and can continue to receive valid capture events.
Once blocked, a merchant remains blocked for the rest of the input. A later merchant update with a risk score below 80 does not unblock the merchant.
FastPrep callable adapter
For the FastPrep callable adapter, implement processEvents with one events array parameter. The array contains the n event strings (n == events.length), so the separate count line used by custom STDIN is not passed to the function. Language-specific starter code provides the corresponding parameter type.
Function
processEvents(events: String[]) → String[]Examples
Example 1
events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,create,merchant_2,-","evt_3,2,payment,payment_1,authorize,-,10","evt_4,3,payment,payment_2,authorize,-,25","evt_5,4,payment,payment_1,capture,-,10","evt_6,5,payment,payment_2,capture,-,5"]return = ["payment_1,merchant_1,successful,10,10,4","payment_2,merchant_2,partially_captured,25,5,5"]Example 2
events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,create,merchant_2,-","evt_3,2,payment,payment_1,authorize,-,10","evt_4,3,payment,payment_1,authorize,-,10","evt_5,4,payment,payment_2,authorize,-,15","evt_6,5,payment,payment_1,capture,-,10","evt_6,5,payment,payment_1,capture,-,10","evt_7,6,payment,payment_2,capture,-,15"]return = ["payment_1,merchant_1,partially_captured,20,10,5","payment_2,merchant_2,successful,15,15,6"]Note: The amount_captured is not updated twice since the last event has an ID that was already processed.
Example 3
events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,capture,-,20","evt_3,2,payment,payment_1,capture,-,10","evt_4,3,payment,payment_2,create,merchant_2,-","evt_5,4,payment,payment_1,authorize,-,10","evt_6,5,payment,payment_2,authorize,-,20","evt_7,6,payment,payment_1,capture,-,10","evt_8,7,payment,payment_1,authorize,-,5","evt_9,8,payment,payment_2,capture,-,10","evt_10,9,payment,payment_2,capture,-,10"]return = ["payment_1,merchant_1,successful,10,10,6","payment_2,merchant_2,successful,20,20,9"]Note: evt_5 is ignored because a capture is not valid for a created payment. evt_6 is ignored because an authorize is not valid for a partially_captured payment.
Example 4
events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,create,merchant_2,-","evt_3,2,payment,payment_1,authorize,-,10","evt_4,3,payment,payment_2,authorize,-,20","evt_5,4,payment,payment_1,capture,-,10","evt_6,5,payment,payment_2,capture,-,20","evt_7,6,refund,payment_1,-,-,-","evt_8,7,refund,payment_2,-,-,-","evt_9,8,refund,payment_1,-,-,-"]return = ["payment_1,merchant_1,refunded,10,10,6","payment_2,merchant_2,refunded,20,20,7"]Example 5
events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_1,authorize,-,10","evt_3,2,merchant_update,-,-,merchant_1,85","evt_4,3,payment,payment_2,create,merchant_1,-","evt_5,4,payment,payment_2,authorize,-,20","evt_6,5,payment,payment_1,capture,-,10","evt_7,6,merchant_update,-,-,merchant_1,10"]return = ["payment_1,merchant_1,successful,10,10,5","payment_2,merchant_1,created,0,0,3"]Constraints
- Events with the same
event_idshould not be processed again. - A payment will only ever be attached to a single merchant.
- You can assume that the amount captured will never exceed the amount authorized.
- Numeric columns default to
0when no value has been accumulated (e.g.payment_1,merchant_1,created,0,0,0) - Output payments in the order they were created (i.e., the order their
createevent was first processed) - Each merchant has a risk score represented by an integer from
0through100, inclusive. - Once blocked, a merchant remains blocked for the rest of the input. A later merchant update with a risk score below
80does not unblock the merchant.