Falcon¶
Why¶
Falcon is an open source project for charities.
Quick start¶
Follow instructions in Installation section.
Installation¶
Install Docker¶
You need to install Docker and Docker-compose. See https://docs.docker.com/install/ for details.
Make local environment¶
Open command line terminal, navigate to project root and run make install
.
If you want to adjust local environment settings then first run make stop
and review the .env
and .docker/docker-compose.override.yml
files.
After that run make install
.
Then you can stop your work by running make stop
and continue by running
make up
. To remove all the containers you can run make down
.
Update local environment¶
If there were some changes in the upstream that render your local environment
settings out of date - you need to update the .env and
docker/docker-compose.override.yml
files. There are 2 ways to do it - just
remove them and run any make
command or review them manually and compare with
the sources:
.env
file comes from.env.default
.docker/docker-compose.override.yml
comes from.docker/docker-compose.override.default.yml
.
Access the site¶
Go to http://frontend.docker.localhost to see the frontend and http://admin.docker.localhost to access the backend.
Configuration management¶
General approach¶
All configuration is stored in code. Any configuration change done directly on production environment will be reset on next deploy.
To allow clients to change configuration we should use the Config Pages module approach.
Testing¶
Overview¶
Falcon uses Codeception framework to test its functions, modules, features and API endpoints. Tests are integrated in development workflow using Circle CI.
Every time when someone opens a new pull request on Github, Circle CI executes the following tasks:
- Checkout code from the corresponding git branch.
- Spin up docker containers.
- Install Falcon and all its features.
- Run tests.
If you are interested in details of Falcon CI setup, you can find latest Circle CI configuration file here: https://github.com/systemseed/falcon/blob/master/.circleci/config.yml
Running tests on local¶
First, run make tests:prepare
to initialize testing framework.
Now, you can run all Codeception tests using make tests:run
command. You can
run a specific test suite by passing its name as a second parameter:
# Run API test suite only
make tests:run api
# Run unit test suite only
make tests:run unit
You can pass any extra options to codecept
bin if you need:
# Run API tests from group "failed".
make tests:run api -- -g failed
Tests structure¶
There are two Codeception test suites in /tests
folder: API and Unit.
Both have connection to Drupal 8 API and to the database which allows developers to
implement sophisticated and granular tests.
API tests¶
This type of tests is suitable for testing of API (REST) endpoints. You can find examples of API tests here: https://github.com/systemseed/falcon/tree/master/tests/api
Read more: https://codeception.com/docs/modules/REST#Actions
Unit tests¶
Suitable for classic unit tests and integration tests with runtime access to Drupal code and database.
You can find examples of unit tests here: https://github.com/systemseed/falcon/tree/master/tests/unit
Read more: https://codeception.com/docs/05-UnitTests
Writing tests¶
Each new feature, or API endpoint, or pure PHP function should be covered by tests.
When adding a new test to the Falcon distribution developer should decide if
it will cover basic distribution installation or not.
If it covers basic installation distribution it should be added to basic
tests group. If not than it should be added to additional
group.
For example:
/**
* @group basic
*/
public function testBasic()
If you wrote a test for additional
group than you should add module this test covers into ADDITIONAL_MODULES
variable in env.default
file.
It’s recommended to store tests in subfolders with the same name
as Falcon module or feature to test. For example, if you want to
test function falcon_development_install
you need to put your test into
tests/unit/falcon_development/
folder.
Before you start writing tests we recommend to run the following command:
# Optional. Creates ".codecept" folder with Codeception sources in project root.
make tests:autocomplete
It will enable autocomplete of available test methods in most of popular IDEs.
Now you can start writing tests and run them using make tests:run
.
To access codecept
cli directly run:
make tests:cli
It will allow you to run codecept commands to generate new tests or run a specific test if you need. See list of available commands here: https://codeception.com/docs/reference/Commands
Contributing¶
Coding Standards¶
Falcon follows Drupal 8 Coding standards.
To check code on your local environment, run the following commands:
make code:check
- checks all PHP & Javascript code against Drupal and DrupalPractice coding standards.make code:fix
- attempts to automatically fix some of found issues in PHP & Javascript code.
Documentation¶
We’d love to have you helping with Falcon documentation.
All documentation is stored in the main Falcon repo in docs
folder. We use
reStructuredText format and readthedocs.org hosting for our documentation.
Getting started¶
You can start contributing using GitHub edit feature. All you need to do is to clone the repo, edit one of .rst files and submit PR as usual.
Haven’t used reStructuredText before? Don’t worry! Check out a couple of examples to get started.
Building on local¶
If you’d like to build Falcon docs on your local please uncomment sphinxdocs
service in docker-compose.override.yml
and run make up
.
Now all your changes in docs
folder are tracked and automatically built in
docs/_build/html
folder.
Falcon Development¶
Naming conventions¶
Custom modules should be placed into the modules/custom directory and prefixed with falcon_ string, for instance falcon_example.
Packaged features should be placed into modules/features directory and prefixed with falcon_feature_ string, for instance falcon_feature_example.
Adding a new module¶
When adding a new module to the Falcon distribution developer should decide if it is needed on every site consuming the distribution. If this is the case then it should be enabled using hook_update_N() in the falcon_deploy module and also added as a dependency into falcon.info.yml file so it gets installed on new installations. If the module is not required on every site then it should either be a dependency for some other modules/features or enabled by the team managing specific client site.
Falcon Dashboard¶
Falcon provides a lightweight administration dashboard for users who don’t need full access to all Drupal administration tools.
The dashboard is available at /dashboard
and integrated with
Admin Toolbar.
Permissions¶
- Users need “Use the administration toolbar” permission to access Dashboard from the toolbar.
- Users need “Use default admin toolbar” permission to access Administration menu from the toolbar.
There are two ways to place new categories and items on the dashboard:
- Export config menu items into features using Config menu link module. Preferred option for Falcon features.
- Add normal menu items (content level) and flush cache.
Creating releases¶
Requirements¶
- New release should be based on master branch.
- New release tag should look like “1.0.0”.
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes.
- MINOR version when you add functionality in a backwards-compatible manner.
- PATCH version when you make backwards-compatible bug fixes.
You can read documentation for more info about version standardization.
Create new release¶
- Make sure you switched to the master branch and have the latest version of it.
- Create a new tag
git tag -a 1.1.0 -m "Version 1.1.0"
- Push a new tag to github.com
git push origin 1.1.0
- Go to Releases page on github and Draft a new release
- Choose tag version, add Release title (e.g. “1.1.0 (March 7, 2019)”), mention main changes since last release in Description (e.g. 1.1.0) and publish the release.
You can read official Github instruction for more info how to create releases.
Client-site Development¶
General approach¶
Falcon is using Features module to package configurations. Please see Features handbook for detailed information on Features.
When feature is installed for the first time its configuration files are getting imported into site configuration - once it is done then this configuration is owned by the site.
It is recommended to use the Config Distro module to manage configuration updates coming from Falcon distribution.
How to install Falcon feature?¶
Go to /admin/modules and install it.
How to update Falcon feature?¶
After updating the codebase to the new version of Falcon go to /admin/config/development/configuration/distro and import feature updates.
TODO: check if it can be done automatically in hook_update_N() or using drush command config-distro-update.
How to remove Falcon feature?¶
Go to /admin/modules/uninstall and uninstall it. Then go to /admin/config/development/features, review the configurations supplied with the uninstalled feature and remove the configurations you don’t need anymore.
TODO: Is there (or can we provide) a way to automate this through hook_update or some drush command? There should be some automated upgrade path from old configs to new configs list. Maybe through hook_update or something like that.
Information Architecture¶
This section contains Information Architecture for the Falcon project. It helps to understand the relationships between entities alongside with their key fields and design choices.
Donations¶
Appeal¶
Appeal is a revisionable node, which represents content for Appeal Landing Page, alongside with settings for the donation form and Thank You Landing Page / Email. Here’s the list of key fields divided into groups on the edit form:
General Appeal Information: | |
---|---|
|
|
Appeal Landing Page: | |
|
|
Donation Form: | This section defines behavior of donation form on the appeal’s landing page.
|
Source Codes: | Note: This section is relevant only when `Falcon ThankQ` integration is enabled. This section uses Inline Entity Form to embed Source Code entity inline as a multiple unlimited field. Each Source code entity has the following set of fields:
|
Thank You Landing Page: | |
|
|
Thank You Email: | |
|
Commercial part of Donations¶
This section describes IA of Commerce system which supports Donations in Falcon.
Commercial part of Donations is based on Drupal Commerce module. All default commerce entities generated by this module get deleted upon installation of Falcon Commerce module.
Store: | There is a single General Store available for all commerce operations on the backend. |
---|---|
Order types: | Donations has its own Order type called Donation. Here’s list of fields:
|
Order workflow: | NOTE: Drupal Commerce doesn’t allow order workflows without draft state and without place transition. The system defines only 1 workflow called Donation with the following states:
|
Order Item type: | |
There is single order item type Donation [donation]. Every Order Item defines the following fields:
|
|
Product type: | The donation feature implementation defines only 1 product type called Donation with a single product also called Donation with a 0 price. This product is created automatically on Falcon installation and is used for adding to the Order Item with overriding of its price upon order creation. The single donation product has hard-coded product SKU donation which makes the interaction with it easier. |
Product variation type: | |
The donation feature implementation defines only 1 product type variation called Donation with a single product also called Donation with a 0 price. This product is created automatically on Falcon installation and is used for adding to the Order Item with overriding of its price upon order creation. The single donation product has hard-coded product SKU donation which makes the interaction with it easier. |
|
Payment gateways: | |
Default Drupal Commerce’s payment gateways. |
|
Customer Profile: | |
Donations implementation in Falcon extends the default Customer Profile type called Customer. This profile type allows for multiple profiles of the same type for the same user. It has the following fields:
|
Users¶
This section defines structure of user accounts & profiles within the system.
- Email - default Drupal’s user email address.
- Account name - default Drupal’s user account name field.
- First Name - optional text field with donor’s First Name.
- Last Name - optional text field with donor’s Last Name.
- Status - default Drupal’s status field. All donors get their own user account, but status is set to 0 to restrict from authentication.
Roles & Permissions¶
This section contains definition of user roles and their level of permissions within the system.
Administrator: | Full administrative access to all sections of the site, including modules & other development aspects. This role is defined for developers. |
---|---|
Manager: | Full access over site’s content, users and all features without access to development sections. This role is defined for people with highest level of access within the site. |
Content manager: | |
Full access to all content of the site, apart from appeals. This role is defined for content editors of a company. | |
Appeals manager: | |
Create, edit & delete access to appeals. This role is defined for staff members, who work only with specific donation campaigns and do not need to have access to other content. | |
Donations manager: | |
View access to donations submitted by donors. This role is defined for staff members, who should be able to overview submitted donation amounts and donors’ data. |
Content API¶
Falcon supports multiple ways to expose content via API:
You are free to use any of the options above to meet your project goals.
JSON:API¶
JSON:API is recommended option for API exposure in Falcon. With JSON:API & JSON:API Extras (both installed in Falcon), you most likely will be able to cover most your API-related tasks.
However, please be aware of well-known JSON:API limitations:
- In case of more complex content structures (nested Media, Paragraphs, etc) JSON:API queries may become hard to maintain.
- There is no way to make cross-bundle requests. Follow this issue to stay up to date.
- JSON:API doesn’t support aliases and redirects out of the box. You will need help of Decoupled Router of similar modules.
Usage:
/jsonapi/node/my-bundle
Read more about JSON:API in the official documentation.
RESTful Web Services¶
With RESTful Web Services module you can enable built-in endpoints for various content types. Default REST endpoints have a couple of drawbacks:
- No way to include related entities.
- No way to expose listings.
Usage:
Go to /admin/config/services/rest
page and enable “Content” resource. Make a request:
/node/1?_format=json
or
/node-alias?_format=xml
Custom resource:
With REST module you can create your own REST endpoints.
Read more about RESTful Web Services in the official documentation.
REST Views¶
You can create a view with REST export display to expose dynamic lists of content via API. Known drawbacks of this approach:
- You have to reconfigure your view if business logic of the app has been changed.
- You have to create similar views for different listings on your site.
Falcon will be return the response with pagination data, but if you want to use page
or item_per_page
in your request you should configure exposed options in you pager options.
In Falcon, we have enhanced pagination support for REST Views (via patch).
Example request:
/view-url?_format=json&page=3
Read more about JSON:API in the official documentation.
Rest Entity Recursive¶
Rest Entity Recursive provides new “json_recursive” REST format which exposes all fields and referenced entities by default. You may want to use this module if you need to fetch almost everything in one request.
Usage:
Enable core REST resource or create REST View display with format json_recursive
enabled. Make a request:
/request-url?_format=json_recursive
You can use query parameter max_depth
if you want to limit depth of loaded references. Default is max_depth = 10
.
Find examples how to customize and fine-tune the output in submodules:
- rest_media_recursive
- rest_paragraphs_recursive
Read more about Rest Entity Recursive on Drupal.org project page.
Payments / Checkout API¶
Falcon uses Commerce Decoupled Checkout module as an endpoint for all Commerce order creation and payments.
The REST endpoint is /commerce/order/create
.
You can find the link about the expected payload and additional payment related endpoints in the module’s documentation.
Below you will find more examples how those endpoints are used on Falcon for various use cases and payment gateways.
Additionally, you can look at tests inside of ./tests/api/commerce_decoupled_checkout
folder for real payment examples.
Supported Payment Gateways¶
Out of the box Falcon comes with support of the following payment methods:
- Paypal (disabled by default)
- Credit Cards through Stripe (disabled by default)
- Credit Cards through Realex / Global Payments (disabled by default)
- Direct Debits (enabled by default)
- Example payment (disabled by default)
In fact, thanks to Commerce Decoupled Checkout any other Drupal module which provides payment gateway for Drupal Commerce is supported as well.
Donations API example¶
Here’s the example REST Payload to submit a new donation with Example Payment method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | {
"order": {
"type": "donation"
"field_appeal": 1 // Appeal ID. Mandatory.
"order_items": [
{
"type": "donation",
"field_donation_type": "single_donation", // Donation type. Can be "single_donation" or "recurring_donation".
"purchased_entity": {
"sku": "donation"
},
"unit_price": {
"number": 15,
"currency_code": "EUR",
}
}
],
},
"profile": {
"field_phone": "88001234567",
"field_contact_email": 1,
"field_contact_phone": 0,
"field_contact_post": 0,
"field_contact_sms": 0,
"address": {
"given_name": "John",
"family_name": "Snow",
"country_code": "US",
"address_line1": "1098 Alta Ave",
"locality": "Mountain View",
"administrative_area": "CA",
"postal_code": "94043"
}
},
"user": {
"mail" : "test@systemseed.com",
},
"payment": {
"gateway": "example_test", // Machine name of the payment gateway added by admin.
"type": "credit_card",
"details": {
"type": "visa",
"number": "4111111111111111",
"expiration": {
"month": "01",
"year": "2022"
},
},
}
|
Paypal Payments¶
Requires Commerce Paypal Express Checkout module to be enabled & configured (already part of Falcon).
To make Paypal payments, the first step is to send request to standard endpoint /commerce/order/create
with order / user info (no payments details yet).
This request will return created Order ID - save it into a variable. The further step depends on Single vs Recurring payment.
In case of Single Payment, send this data to /commerce/payment/create/$orderID
:
1 2 3 4 5 6 7 8 9 10 11 12 | {
gateway: "paypal_ec_test",
type: "paypal_ec",
details: {
type: 'single',
data: {
transactions: [{
description: 'Single donation.',
}],
},
}
}
|
This response will initialize a payment transaction and return created Payment ID - save it as well. Next, the confirmation of the transaction should happen on the client side by a user. See Paypal Checkout for detailed documentation of frontend implementation.
When a user confirms transaction on the frontend, send this payload to /commerce/payment/capture/$orderID/$paymentID
.
It should finalize the payment transaction.
In case of Recurring Payment, send this data to /commerce/payment/create/$orderID
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | {
gateway: "paypal_ec_test",
type: "paypal_ec",
details: {
type: 'single',
data: {
billing_plan: {
name: 'Monthly donation',
description: 'Monthly donation for my website.',
type: 'INFINITE',
payment_definitions: [{
name: 'Monthly donation',
type: 'REGULAR',
frequency: 'MONTH',
frequency_interval: 1,
cycles: 0,
}],
merchant_preferences: {
auto_bill_amount: 'NO',
initial_fail_amount_action: 'CONTINUE',
max_fail_attempts: 0,
},
},
billing_agreement: {
name: 'Monthly donation for my website',
description: 'Description of your donation.',
},
},
}
}
|
The next step is the same as with one-off payment - let a user verify the transaction on the frontend and then send the received
payload to /commerce/payment/capture/$orderID/$paymentID
.
The structure & available options for the payment initialization of Paypal payments follow Paypal PHP SDK library. Here is the code which transforms the data from the frontend request into Paypal-acceptable format: Single Payment and Recurring Payment.
Realex (Global Payments)¶
Requires Commerce Global Payments (Realex) module to be enabled & configured (already part of Falcon).
The request should contain data related to order, user and payment. Send the request to /commerce/order/create
.
Here’s the example of payload part specific to Global Payments (Realex) payment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {
// All order & user specific data.
...
// Payment details.
payment: {
gateway: "globalpayments_creditcard_test", // Machine name of payment gateway added by admin.
type: "globalpayments_credit_card",
details: {
name: "John Snow",
number: "4263970000005262",
security_code: "123",
expiration: {
month: "02",
year: "2023",
}
}
}
}
|
Stripe Payments¶
Requires Commerce Stripe module to be enabled & configured (already part of Falcon).
To make a payment using Stripe, you need to obtain a Stripe token first. It is up to the frontend application to handle it.
For example, React.js has react-stripe-checkout library which handles it for you (token
method).
Another example for PHP you can find in ./tests/api/commerce_decoupled_checkout/StripeCest.php
.
As soon as you got the token, the remaining step is straightforward - just send the request to /commerce/order/create
.
Here’s the example of payload part specific to Stripe Payments:
1 2 3 4 5 6 7 8 9 10 11 12 | {
// All order & user specific data.
...
// Payment details.
payment: {
gateway: "stripe_test", // Machine name of payment gateway added by admin.
type: "credit_card",
details: {
stripe_token: "<INSERT_TOKEN_HERE>",
}
}
}
|
Direct Debit Payments¶
Direct Debits are enabled by default. The request should contain data related to order, user and payment.
Send the request to /commerce/order/create
. Here’s the example of payload part specific to Direct Debit payment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {
// All order & user specific data.
...
// Payment details.
payment: {
gateway: "direct_debit_test", // Machine name of payment gateway added by admin.
type: "direct_debit_sepa", // Can be "direct_debit_sepa" or "direct_debit_uk"
details: {
account_name: "John Snow",
swift: "BOFIIE2D",
iban: "DE89 3704 0044 0532 0130 00",
debit_date: 2,
accept_direct_debits: 1,
one_signatory: 1
}
}
|
Payment Test / Live modes¶
Every payment gateway has live and test payment modes.
Falcon allows to use test payment modes on any non-production environments. For the production environment test payments are restricted. To use test payment mode on production environment you need to set special environment variables: PAYMENT_SECRET_HEADER_NAME and PAYMENT_SECRET_HEADER_VALUE - and then set local storage value in the browser using the supplied name and value.
Example:
PAYMENT_SECRET_HEADER_NAME = X-Payment-Secret
PAYMENT_SECRET_HEADER_VALUE = 76a67787-af11-4870-b384-b8e85c4fe3b8
And then browser local storage should have X-Payment-Secret / 76a67787-af11-4870-b384-b8e85c4fe3b8
Environment¶
Falcon is using the environment variable called ENVIRONMENT to recognize if it is running in production mode.
Supported values are:
- production - if the value of ENVIRONMENT is production then Falcon is running in production mode. If it has any other value - then it is running in non-production mode.
- development - if the value of ENVIRONMENT is development then Falcon is running in development mode.
In order to use this environment variable on the PHP code level you need to set it. Local environment powered by Docker and Makefile sets it for the php service in the .docker/docker-compose.override.yml file taking the value from the .env file. To set this environment variable on the hosting environment you should use the hosting provider tools.
There is a Drupal service to use in any custom code to determine the environment - falcon_common.environment. You can inject it and then use like this: $this->environment->isProduction().
1 2 3 4 | services:
module_name.service_name:
class: Drupal\module_name\ServiceName
arguments: ['@falcon_common.environment']
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <?php
namespace Drupal\module_name;
/**
* Class ServiceName.
*/
class ServiceName implements ServiceNameInterface {
/**
* The environment.
*
* @var \Drupal\falcon_common\EnvironmentInterface
*/
protected $environment;
/**
* Constructs a new Payment object.
*
* @param \Drupal\falcon_common\EnvironmentInterface $environment
* The environment.
*/
public function __construct(RequestStack $request_stack, EnvironmentInterface $environment) {
$this->request = $request_stack->getCurrentRequest();
$this->environment = $environment;
}
public function methodName() {
$if (this->environment->isProduction()) {
// Do smth related to production environment.
}
}
}
|
Content¶
Basic concepts¶
- Falcon follows Drupal best practices to organise content structure: nodes, fields, taxonomy, etc.
- Each content type has at least title (standard Drupal property), image (
field_image
) and short description (field_description
) fields. - Falcon uses Paragraphs to manage visual content (
field_blocks
). - Falcon doesn’t require to follow these concepts. Instead, it ships with a showcase to demonstrate how these concepts allow building flexible and sophisticated content architectures.
Showcase¶
Falcon demo content structure is stored in the following features:
falcon_feature_content_structure_demo
- collection of content types with fields and relationships.falcon_feature_content_blocks
- collection of content blocks for the demo content types.- Default content is coming soon.
- Frontend showcase is coming soon.
Content blocks¶
Content blocks built using Paragraphs module.
Paragraphs Browser¶
Content blocks are organised in groups using Paragraphs Browser module. Falcon content showcase includes the following groups:
Hero¶
The blocks at the top of the page, usually a full width image with headings / links.
Section¶
Containers for other blocks (items).
Items¶
Can be added to sections blocks only (i.e. child blocks).
Widgets¶
Rich interactive elements.
Listing¶
Automated / semi-automated listings, i.e. “Latest content” block.
Paragraph machine name should begin with its group name, i.e. “Latest content” block
should be named listing_latest_content
.
Reusable content blocks¶
Reusable content blocks use Paragraphs Library module from Paragraphs package in combination with Entity Browser for better editorial experience.
Media¶
Falcon Media tools are based on Drupal core and fully compatible with Media Browser. It is recommended to use Media fields instead of legacy Image/File fields when developing sites on Falcon.
Basic media configuration should be stored in Falcon Media feature. It provides setup for images (all popular formats and SVG) and videos (local and external).
Note: Falcon uses Video Embed Field solution for external videos until [#2996029] Add oEmbed support to the media library is solved.
Metatags¶
Falcon provides metatags. You can create new metatag field into entity type, and you can get metatag_normalized key from jsonApi. Also falcon use consumers_token module that provides a token [consumers:current-name] for consumers name replacement depending on what consumer has requested the token.
Email Configuration¶
Falcon mailing functionality is placed in falcon_mail
module. The module provides a plugin for Drupal mail system with a formatter that can wrap emails into HTML templates and replace dynamic tokens.
You can change formatter on admin/config/system/mailsystem
page.
You can configure you email with several params. Example
$to = $order->getEmail();
$langcode = 'en';
$reply = NULL;
$send = TRUE;
$params = [];
$params['from'] = 'your_email@example.com'
$params['subject'] = "Message subject";
$params['body'] = "<div> Html message body </div>";
$params['headers'] = ['Content-Type' => 'text/html']; # Enable html. You can pass any headers.
$params['render_tokens']['commerce_order'] = $order; # Array 'render_tokens' contains variables for token replacement.
$params['token_options'] = [ # Array 'token_options' contains options for token replaceement.
'langcode' => $langcode,
'callback' => 'callback_function'
];
$params['replace_tokens'] = TRUE; # Enable replacement tokens.
$params['theme_template'] = "<div> #$#BODY#$# </div>" # The template where will be replaced #$#BODY#$# on $messsage['body'].
\Drupal::service('plugin.manager.mail')->mail('your_module', 'your_mail_key', $to, $langcode, $params, $reply, $send);
The theme_template
param can use the #$#BODY#$# variable for wrap an another html to theme_template
html.
You can also find an example of working in the falcon_donation
module.