SpreeCmCommissioner
An application platform built on top of Spree commerce for modeling any bussiness applications.
Installation
1; Add this extension to your Gemfile with this line:
gem 'spree_cm_commissioner'
2; Install the gem using Bundler
bundle install
3; Copy & run migrations
bundle exec rails g spree_cm_commissioner:install
4; Restart your server
If your server was running, restart it so that it can find the assets properly.
Bump a new version
Update the semantic version number in lib/spree_cm_commissioner/version.rb
, then create and push a new tag to GitHub. When the tag is pushed, the GitHub Actions workflow will automatically build the gem and publish it to RubyGems.
Config
Rake tasks
# Seed province data for Cambodia country
rake data:seed_kh_provinces
# Seed option values and type location
rake data:seed_kh_location_option_values
# Reindex Elasticsearch on Vendor model
rake searchkick:reindex CLASS=Spree::Vendor
Google Map
CM commissioner required Google Map key for map components.
# .env
GOOGLE_MAP_KEY = ""
DEFAULT_LATLON = "10.627543,103.522141"
Elasticsearch
Commissioner required elasticsearch version 8.5.2. We recommend using evm to manage their version.
1, Install EVM (Elasticsearch Version Manager):
sudo curl -o /usr/local/bin/evm https://raw.githubusercontent.com/duydo/evm/master/evm
sudo chmod +x /usr/local/bin/evm
2, Install elasticsearch
evm install 8.5.2
# To start elasticsearch
evm start
# To stop elasticsearch
evm stop
Visual Studio Code Editor && Rubocop
Install VScode Extensions: ruby-rubocop
Make sure have below settings in VScode User Settings. It will auto-correct with rubocop after save file
{
"ruby.rubocop.executePath": "/Users/USER_NAME/.rbenv/shims/"
}
- We can run auto correction
# Autocorrect offenses (only when it's safe)
$ bundle exec rubocop -a # or bundle exec rubocop --auto-correct
# Autocorrect offenses (safe and unsafe).
$ bundle exec rubocop -A # or bundle exec rubocop --auto-correct-all
All environments
Following are required varialbles inside .env
GOOGLE_MAP_KEY = ""
DEFAULT_LATLON = "10.627543,103.522141"
ACCOMMODATION_MAX_STAY_DAYS = 10
DEFAULT_TELEGRAM_BOT_API_TOKEN = ""
PIN_CODE_DEBUG_NOTIFIY_TELEGRAM_ENABLE="yes"
RECAPTCHA_TOKEN_VALIDATOR_ENABLE="yes"
EXCEPTION_NOTIFY_ENABLE="yes" # yes or no
EXCEPTION_TELEGRAM_BOT_TOKEN=""
EXCEPTION_NOTIFIER_TELEGRAM_CHANNEL_ID=""
JSON Format Examples For Store and Tenant Preferences
Asset Links Format
For Android app integration, use the following format in your assetlinks.json
:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourapp.app",
"sha256_cert_fingerprints": [
"SHA256_CERTIFICATE_FINGERPRINT_HERE"
]
}
}
]
Apple App Site Association Format
For iOS app integration, use the following format in your apple-app-site-association
:
{
"webcredentials": {
"apps": [
"TEAM_ID.com.yourapp.app"
]
},
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.com.yourapp.app",
"paths": ["*"]
}
]
}
}
Using Deface DSL (.deface files)
- Make sure the path of override should match the path of view template
- The .deface can be use with :erb, :html, or :text
Example:
View Template file: app/views/spree/admin/vendors/_form
Override file: app/overrides/spree/admin/vendors/_form/logo.html.erb.deface
https://github.com/spree/deface#using-the-deface-dsl-deface-files
Schedule Jobs
- Create a schedule to update vendor min and max price
- Frequently: every 24 hours
- Run time: mid night is preferable
- Command:
rake "spree_cm_commissioner:vendor_update_price_range"
- Customer_notification
customer_notification:
cron: '0 0 * * * *' # will trigger every hour, every day of the month, every month, and every day of the week
class: SpreeCmCommissioner::CustomerNotificationCron
Testing
First bundle your dependencies, then run rake
. rake
will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using rake test_app
.
bundle update
bundle exec rake test_app
When testing your applications integration with this extension you may use it's factories. Simply add this require statement to your spec_helper:
require 'spree_cm_commissioner/factories'
Run test with Github action
We are utilizing GitHub Actions workflows defined in .github/workflows/publish.yml.
Run test with docker
Our project includes both a Dockerfile and a docker-compose.yml configuration. If you require more fine-grained control over your workflow, including environment setup and database services, use docker-compose to build the images and run them in one go. However, you can also configure docker-compose to run tests with your external database installation.
Note: By default, we use a local instance of PostgreSQL.
To manually build and run the Docker image with an external database:
> docker build -t central-market-comissioner_web .
> docker run -e DATABASE_URL=postgres://postgres:@192.168.1.136:5432/spree_cm_commissioner_spree_test -e RAILS_ENV=test -it central-market-comissioner_web bash
> bundle exec rake
To build and run the project with docker-compose:
docker-compose build
docker-compose up --build
This setup allows you to configure the environment and database services in a single step, making it easier to manage and run tests.
Releasing
To release the gem:
1; Bump new version in the file: lib/spree_cm_commissioner/version.rb, for example
VERSION = '1.8.0-beta1'.freeze
to release 1.8.0-beta1
2; Tag a repo with the same version, for example
git tag 1.8.0-beta1
3; Push the tag to the repo and github action will build and push the gem to rubygem.org.
4; Get the gem version to the project in cm-market-server.
Troubleshooting Database Lock on Taxonomy Creation Errors
Issue Overview
In development, this error can occur after creating a new database and restoring the database (e.g., from a staging environment). When this happens, attempts to create a new Taxonomy may fail due to a record lock.
Error Message
In case you create in console manualy
/usr/local/bundle/gems/activerecord-7.0.8/lib/active_record/locking/pessimistic.rb:70:in `lock!': Locking a record with unpersisted changes is not supported. Use `save` to persist the changes, or `reload` to discard them explicitly. (RuntimeError)
In case you create from web-app
# Locking a record with unpersisted changes is not supported. Use save to persist the changes, or reload to discard them explicitly.
# Extracted source (around line #70):
if persisted?
if has_changes_to_save?
raise(<<-MSG.squish)
Locking a record with unpersisted changes is not supported. Use
`save` to persist the changes, or `reload` to discard them
explicitly.
Workaround Solution
- Open the Rails console:
rails console
- Find an existing Taxonomy record:
taxonomy = Spree::Taxonomy.last
- Update any attribute to release the lock. For example:
taxonomy.update(name: "#{taxonomy.name}1")
This action should unlock the record, allowing you to successfully create a new Taxonomy.
Contributing
If you'd like to contribute, please take a look at the instructions for installing dependencies and crafting a good pull request.
Copyright (c) 2022 [name of extension creator], released under the New BSD License
Account Deletion Cron Job
- AccountDeletionCronJob
- Frequently: every 24 hours
- Deleted Account will last for 1 month before it is permanently deleted
Multiple databases
In most cases, Rails is able to infer the database connection. However in some instances, for example, in the spree_backend gem, it uses the GET request to destroy the session which in turn triggers database update that require the writing role. To fix this we need to explicitly tell Rails to use the right database connection.
Error using a wrong database connection looks like this:
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
An ActiveRecord::ReadOnlyError occurred in orders#new.
module SpreeCmCommissioner
module Admin
module UserSessionsControllerDecorator
def self.prepended(base)
# spree_devise_auth gem use get as destroy
# get '/logout' => 'user_sessions#destroy', :as => :logout
base.around_action :set_writing_role, only: %i[destroy]
end
end
end
end
unless Spree::Admin::UserSessionsController.ancestors.include?(SpreeCmCommissioner::Admin::UserSessionsControllerDecorator)
Spree::Admin::UserSessionsController.prepend(SpreeCmCommissioner::Admin::UserSessionsControllerDecorator)
end
Avoid Excessive Looping
This test ensures that the query for fetching events does not loop excessively for each item.
Test Explanation
The test is structured as follows:
- Capture Rails Logger: It captures the Rails logger to inspect logs generated during the test execution.
- Execute the Query: It executes the query to fetch events.
- Expectation 1: It expects the query execution to complete without raising any errors.
- Expectation 2: It checks the captured Rails logs for any indication of excessive looping. This is done by verifying that the log messages do not include the phrase "Loop detected".
- Restore Original Rails Logger: Finally, it restores the original Rails logger.
Test Code (Ruby RSpec)
it 'should not loop excessively for each item' do
# Capture the Rails logger to inspect logs
logs = StringIO.new
Rails.logger = Logger.new(logs)
# Execute the query
query = SpreeCmCommissioner::DashboardCrewEventQuery.new(user_id: user_a.id, section: 'incoming')
# Expectation: The query should complete without raising any errors
expect { query.events }.not_to raise_error
# Expectation: Check Rails logs for excessive looping
expect(logs.string).not_to include('Loop detected') # Adjust this log message according to your implementation
# Restore the original Rails logger
Rails.logger = ActiveSupport::Logger.new(STDOUT)
end
Protected Cloudfront with signed requests
To use signed cookies with CloudFront, you'll need to set up your CloudFront distribution to require signed cookies and then generate and distribute the signed cookies to your users. Here's how to do it using Terraform for infrastructure setup and Ruby for cookie generation.
Generating keypair
Generate private key
openssl genrsa -out private_key.pem 2048
Use the private_key.pem to generate the public key
openssl rsa -pubout -in private_key.pem -out public_key.pem
Using the keypair i Cloudfront
Currently this process can not be done directly with terraform due to the fact that Cloudfront use a global region and our Terraform script works with infrastructures in a specific region.
- Go to Cloudfront -> Select Key management and then add a key using your public key
- On the left navigation menu -> Select Keypair with the public key create in the step 1.
- Copy the id of the key pair
Using signed cookie in from Cloudfront
You can send the cookies generated byt the SignedCookies
Future<void> _initializePlayer() async {
final client = http.Client();
final uri = Uri.parse(widget.url);
final request = http.Request('GET', uri)
..headers.addAll({
'Cookie': widget.cookies.entries
.map((entry) => '${entry.key}=${entry.value}')
.join('; ')
});
final response = await client.send(request);
if (response.statusCode == 200) {
_videoPlayerController = VideoPlayerController.network(widget.url);
await _videoPlayerController.initialize();
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
aspectRatio: _videoPlayerController.value.aspectRatio,
autoPlay: true,
looping: true,
);
setState(() {});
} else {
// Handle error
print('Error loading video');
}
}
where the cookies is a
final Map<String, String> cookies;
References
Adaptive bitrate player
ActiveRecord Multi-Tenant
Getting the Current Tenant ID
To retrieve the current tenant_id, you can use MultiTenant.current_tenant_id
.
Defining Scope with MultiTenant
To ensure queries are scoped by the current tenant, use MultiTenant.with(@tenant)
:
def scope
MultiTenant.with(@tenant) do
model_class.where(tenant_id: MultiTenant.current_tenant_id)
end
end
This ensures that queries are filtered based on the current tenant, maintaining proper isolation of data across tenants.
Example: Getting @tenant
with MultiTenant
@tenant = current_tenant
MultiTenant.with(@tenant) do
# Your tenant-specific code here
# Should scope model_class.where(tenant_id: MultiTenant.current_tenant_id)
# to make sure our data filter it with current Tenant
end
Wrapping Actions Without MultiTenant
Use around_action
to temporarily disable MultiTenant for specific actions:
around_action :wrap_with_multitenant_without, except: %i[create]
def wrap_with_multitenant_without(&block)
MultiTenant.without(&block)
end