# Winston's Xfers Best Practice Notes
### ActiveRecord
#### Don't Use ActiveRecord Callbacks if possible
https://engineering.gusto.com/the-rails-callbacks-best-practices-used-at-gusto/
#### Use ActiveRecord Query Rather than Raw SQL if possible
Raw SQL is the way to go for complex query though
https://stackoverflow.com/questions/9715184/is-it-a-good-practice-to-directly-write-sql-query-in-rails
### Job
#### Don't put long data into a job
Our logging logs what is putted into a job. Keep the data there small
You cannot put like Something.perform_now(Array.new(100))
#### Don't use delayed job. it is deprecated
#### Catch Unexpected error
Sidekiq Automatically retries your job in case of an unexpected error. to be more save about the behaviors on unexpected error, catch it and do something/ throw slack notif
https://github.com/mperham/sidekiq/wiki/Error-Handling
### API:
https://docs.google.com/presentation/d/1_K0LlU6kRUI58i1LLR84ul4k__EZtUSDzz65D8KJwQg/edit#slide=id.p
#### Use plural, not singular
/api/v3/withdrawals, not /api/v3/withdrawal
#### Use object, not verb
/api/v3/withdrawals, not /api/v3/withdraws
#### Don't Throw Unexpected Error if you know the reason.
For 500 error, we will log the error data in api_query_logs but
only showing `unexpected error` to customer. So, as much as possible, don't throw 500 error.
#### Provide filtering, sorting, field selection and paging for collections
For List of withdrawals, list of charges, ...
#### Provide idempotency key for post request
Customer need to put a key that sent to us, that when is using same key, we will throw an error. function is to handle
double transactions
#### We also need to give our internal ID as receipt that the request is received.
at each request, we still need to reply customer with our ID as confirmation we have properly recorded the txn.
If customer file a complain, ideally they should use this ID.
This ID should :
1. Not Our database's ID, customer can know how many users/ transaction we have
2. Have identifier, rather than return `xasnkxaks`, we should return something like `user_xasnkxaks`
#### ApiQueryLog is deleted every 3 months
if your API have data that you want to write for logging permanently, at the API you write
AppLogger.ops.info "`controller name` received posting with params of #{params}"
### External Integration
#### Handle Timeout Differently
Don't treat them as failed
Don't treat them as successful.
If you don't want to handle autoretry logic initially, just throw slack error and manually solve first
#### Don't do retry infinitely
If we retry infinitely on a transaction, our partner somehow fucks up the idempotency key,
the potential loss is infinity. We must put a maximum retry
#### Follow the structuring like our existing Client (SampoernaClient)
Main Details:
have Request folder consists of requests ideally for each URL -> make sure the data type is receivable by the
other party, whether it is `query` or `json`, or `xml`
have Response.rb
have FakeResponse.rb
have client.rb -> should also handle the mocking in sandbox and staging
have `something`_api_query_logs to log down request
client_spec.rb should actually hit their sandbox API once and recorded in the VCR
### Database Design
#### String VS Text
Understand when to use string and when to use text/
Text have limit 65535 but cannot have index
String have limit 191 but can have index
#### Zero Downtime Migration
Be very aware of this when altering a table
https://www.notion.so/xfers/Zero-Downtime-Migration-ea7cab3218334438acf154485e50616b
#### tracking changes
use `has_paper_trail only: [:your_column]` if you want to track data change in your table.
It will be shown in the `versions` table
(Your table should be small/not many columns and not changing super often to use this table)
#### state machine
For state machine, if your number of state is limited and state progression is clear, use aasm_state.
if not, use string enum if possible (please understand its difference with number enum) and
String enum:
Ruby:
enum event_name: {
option_1: "option_1",
option_2: "option_2",
}
Database:
option_1
option_2
Number Enum:
enum kyc_status: %i[
option_1
option_2
]
Database:
0
1
(In number enum 0 will map to option_1 and 1 map to option_2, which will be hard to read for the SQL reader)
enum event_name: {
## User Related
connect_token_disabled: "connect_token_disabled",
user_verification_status_updated: "user_verification_status_updated",
### spec
#### use before(:each) and after(:each) mostly
Most of the spec you write will use the each rather than the (:all), or (:suite)
#### The name of unit test should match the path in app
Don't do:
app file in spec/api/v3/contractual_models/users_controller_spec.rb
spec file in spec/api/v3/users_controller_spec.rb
Should do:
app file in spec/api/v3/contractual_models/users_controller_spec.rb
spec file in spec/api/v3/contractual_models/users_controller_spec.rb
#### Freeze the time if necessary
Your spec may be stuffs that fails on certain time, do something like `travel_to(Time.zone.now)`
#### Use factory if possible
If already defined, do create(:user) rathen than User.create!(...)
#### Stub External Call. external hit to external party should be stubbed in spec
stub_request()....
#### Use described_class
in your unit test ,rather than using MyRequest.call(...), use described_class.call(...)
#### don't put logics/ ifs here
don't do something like:
if a == 1
expect(b).to eq "..."
else
expect(c).to eq "..."
end
#### don't expect something to raise specific error
cannot do:
expect(test.perform_now).to raise_error(InsufficientBalanceError)
should do:
expect(test.perform_now).to raise_error
#### If possible, don't use allow function to .. in integration test.
The below should not be written in integration test (can do in unit test)
allow(Sobatku::Client).to receive(:debit_account).and_return(fake_response)
### Other
#### Don't use recursion
It is hard to mentally read it
#### Don't write commented code
https://kentcdodds.com/blog/please-dont-commit-commented-out-code
#### Don't use eval
eval(...) should not be used, have some SQL injection issue
#### Folder your files to separate from other country
In Ideal world, we want to make our files become something like:
- Core
- Indonesia
- ID merchant
- ID merchant 2
- Singapore
- SG merchant
If possible, don't put Indonesia specific or Singapore Specific logic to Core
#### use the time function properly (I think not standardized yet across whole codebase)
Good Examples:
Time.use_current_zone{ 4.hours.ago }.iso8601
Time.in_time_zone(ConfigManager.current_time_zone)
Bad examples:
Time.use_current_zone { Date.tomorrow + 2.hour }
You often goes into trap of time behaving differently in
a. CircleCI server
b. Real Prod Server ID
c. Real Prod Server SG
d. your local