Shopify Requirements
Shopify has a list of technical requirements that payment apps are to support here. This is how we implent and suppport each of those.
Idempotency
As a payments app we need to support idempotency in both directions. Meaning even if Shopify sends us duplicate requests, this cannot result in customers paying twice or merchants refunding twice. On the other side, Shopify guarentees us that duplicate requests made will have no ill side effects.
Shopify to Us
Shopify sends us requests to initiate payments and refunds through our respective /payment
and /refund
endpoints.
Both of these requests inlcude an id
in the request body that serves as the idempotency key. To handle this technical requirement we do a few things within the handlers, database, and transaction building logic.
Handlers - We will only create a single PaymentRecord and RefundRecord for each id
value we receive. When we receive a request, we will first check if we have an existing PaymentRecord or RefundRecord for that id
. If we do, we will then opporate as if we had just created it. Responding with success and returning what ever values we are required to for that initiation.
Database - When creating a PaymentRecord or RefundRecord in the database, we assign the given id
value from shopify as the shopId
on the entry. To prevent duplicate records, we have marked shopId
as unique. This should then result in throwing an error if we try to create another PaymentRecord with the same shopId
as another record in the database.
Transaction Building Logic - An important part of the system design for our payment app is having an open endpoint to generate on chain transactions. This enables us to support Solana Pay Transaction Requests. To prevent double spending, it is not enough to check against the database for the state of a given record. This has to led to us using a conecpt we are calling Single Use Accounts. In each transaction we build for a given record, we generate a deterministic keypair that is generated using the shopId
as input. This keypair is then used in a System Program Create Account instruction which is added to each transaction. Then, even if we end up in situation where multiple transactions are fetched for a single record, only one of those transactions will be able to land on chain.
Us to Shopify
Shopify handles the idepotency on their end when we make requests to them about payments and refunds. We make these calls to Shopify when we "discover" transactions that correspond to PaymentRecords and RefundRecords we maintain. For redundency, we have multiple ways to "discover" these transactions including:
If we end up in a situation where we discover the same transaction twice and then make the same request to shopify twice, Shopify guarentees they will only perform the mutation once and they will response the same both times. This would result in us re-updating the database record with the same update which has no negative effects.
Another thing Shopify guards against is making conflict requests for a given payment and refund. For example, a payment can not be rejected and resolved. If this were to happen, Shopify would notify us within the userErrors field of the response. We will then log this message and treat it according. In our current system design, this should not be possible. The only reason that we will send a rejectPaymentSession request to Shopify is if a customer's wallet address is determined to be "risky" by our Wallet Monitoring partner, TRM. We make this check when a transaction is being fetched from the pay-transaction handler. In this case, we will never return a transaction. If this does happen, we log the reason within Sentry and address the bug.
For refunds, it is more likely for this situation to occur. This is because Merchants have the ability to pay a refund or reject a refund within the merchant portal. The fix here is likely to add some sort of time delay on conflict actions like making a merchant wait 5 minutes to reject a refund they have tried to pay and making a merchant wait 5 minutes to pay a refund they have tried to reject. We can also seperate a merchant send us a message that they want to pay a merchant and then have a forced time delay for actually serving the request. TBD.
Retry Policy
We are required to implement a retry policy on all payment app mutations in case Shopify is not able to handle our requests at any time. This includes:
- resolve payment
- reject payment
- resolve refund
- reject refund
- app configure
We handle this with a message queue using Amazon SQS and AWS Step Functions. On failed requests to Shopify inside process discovered payment transaction and process discovered refund transaction we add a message to the queue. We have set up our sqs message receive handler to get invoked when new messages are added to the queue inside of our serverless.yml file. When that handler is invoked it then reads the message, and uses that as input to invoke our step function which is also defined inside of the serverless.yml file. It will wait for the inputted number of seconds before invoking the retry handler. In the retry handler, we will retry the original call to Shopify. If it fails, we will once again add a message to our message queue to be picked up by the sqs message receive handler.
Mutual TLS (mTLS)
As a part of Shopify's security model, we need to implement mTLS for all requests where Shopify acts as the client and we act as the server. For us that is five cases:
- payment initiation
- refund initiation
- customer data gdpr webhook
- customer redact gdpr webhook
- shop redact gdpr webhook
How we handle this is create two seperate services:
Each of these get deployed to their own API gateway where we can map the respective service to a custom domain name. backend-serverless-green has custom domain name that is configured with mTLS while backend-serverless-purple is configured with TLS.
HMAC verification
HMAC verifiication is one of the security methods shopify uses alongside MTLS and access tokens. It is used during the auth flow with Shopify that happens while installing that app to a merchan't Shopify store. We implent this for both the install and redirect handlers. It happens in the install verify and redirect verify methods respectivly. There is also a testing suite for the install verify and redirect verify methods.
Rate limiting
We currently get the rate limiting values back from Shopify when we make the graphql mutations.
API versioning
We currently support API version 2023-01. We will soon add checks at the begining of our handlers and in the requests to Shopify. After release of the first version, we will start adding support for 2023-04.
GDPR
There are three mantatory APIs that Shopify requires we support.
- customer data request
- customer data redact
- shop redact
Currently, we don't store any info about customers in our app so the first two APIs aren't very important to us. For these, we still
- host them
- protect them with mTLS
- verify they are accessed with correct payloads
For the shop redact, we are still figuring out how this effects our existing records and future refunds. Once those details are discovered, we will likley either immediatly remove items from the database or store the request to proccess these manually.