├── .env.template ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── database └── note.txt ├── package-lock.json ├── package.json ├── public ├── bill-details.html ├── bills.html ├── css │ ├── custom_bootstrap.css │ └── custom_bootstrap.css.map ├── index.html └── js │ ├── bill-details.js │ ├── client-bills.js │ ├── home.js │ ├── link.js │ ├── make-payment-no-tui.js │ ├── make-payment.js │ ├── signin.js │ └── utils.js ├── scss └── custom.scss └── server ├── db.js ├── middleware └── authenticate.js ├── plaid.js ├── recalculateBills.js ├── routes ├── banks.js ├── bills.js ├── debug.js ├── payments.js ├── payments_no_transferUI.js ├── tokens.js └── users.js ├── server.js ├── syncPaymentData.js ├── types.js ├── utils.js └── webhookServer.js /.env.template: -------------------------------------------------------------------------------- 1 | # Copy this over to .env before you fill it out! 2 | 3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys 4 | PLAID_CLIENT_ID= 5 | PLAID_SECRET= 6 | 7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment 8 | # Use 'production' to use real data 9 | # NOTE: To use Production, you must set a use case for Link. 10 | # You can do this in the Dashboard under Link -> Link Customization -> Data Transparency 11 | # https://dashboard.plaid.com/link/data-transparency-v5 12 | PLAID_ENV=sandbox 13 | 14 | # (Optional) A URL for the webhook receiver running on port 8001, to be used 15 | # by Plaid's /sandbox/transfer/fire_webhook endpoint 16 | SANDBOX_WEBHOOK_URL=https://www.example.com/server/receive_webhook 17 | 18 | # If your account is already using transfer, you may have a very large number of 19 | # sync events already that are unrelated to this app! If so, you can set this to 20 | # a higher number to skip over some of these. Otherwise, set this to 0 to start 21 | # from the beginning. 22 | START_SYNC_NUM=0 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | 107 | # VSCode settings (usually altered for screencasting purposes) 108 | .vscode/ 109 | 110 | # User settings 111 | database/appdata.db 112 | database/devappdata.db 113 | database/prodappdata.db 114 | sampleOutput/*.json 115 | .env.dev 116 | .env.sandbox 117 | .env.production 118 | 119 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "proseWrap": "always" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Plaid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transfer Sample App 2 | 3 | The Pay My Bills sample app is a demonstration of how a company (in this case, a 4 | fictional utility company) can use Plaid Transfer to allow their customers to 5 | utilize pay by bank to pay their electric bills. 6 | 7 | This demo app shows two different ways to use Plaid Transfer -- one method using 8 | Transfer UI (which handles several intermediate steps and collects appropriate 9 | proof of authorization), and another method where you have to perform those 10 | steps yourself. 11 | 12 | This app uses NodeJS on the backend (with Express as the server), SQLite as the 13 | database, and plain ol' vanilla JavaScript on the frontend. It designed to be 14 | simple enough that a Python engineer without a lot of deep JavaScript experience 15 | could still understand what's going on and follow along in a video tutorial, so 16 | we avoid too much idiomatic JavaScript. That said, you should be familiar with 17 | [destructuring](https://www.geeksforgeeks.org/shorthand-syntax-for-object-property-value-in-es6/) 18 | and 19 | [object property shorthand](https://www.geeksforgeeks.org/shorthand-syntax-for-object-property-value-in-es6/). 20 | 21 | # Installation 22 | 23 | We recommend having node version 18.x.x or later before attempting to run this 24 | application. 25 | 26 | ## 1. Make sure you have access to Transfer 27 | 28 | First, if you haven't already done so, 29 | [sign up for your free Plaid API keys](https://dashboard.plaid.com/signup). 30 | 31 | If you have a relatively new Plaid developer account, you should already have 32 | access to Transfer in Sandbox. If you don't, please contact support and you can 33 | get access to Transfer in the Sandbox environment without needing to apply for 34 | the full product. 35 | 36 | If you don't have access to Transfer, you can still follow along with this 37 | Quickstart by watching the Video Walkthrough (URL to be added later) 38 | 39 | ## 2. Clone the repository 40 | 41 | Using https: 42 | 43 | ``` 44 | git clone https://github.com/plaid/transfer-quickstart 45 | cd transfer-quickstart 46 | ``` 47 | 48 | Alternatively, if you use ssh: 49 | 50 | ``` 51 | git clone git@github.com:plaid/transfer-quickstart.git 52 | cd transfer-quickstart 53 | ``` 54 | 55 | ## 3. Install the required packages 56 | 57 | Run `npm install` inside your directory to install the Node packages required 58 | for this application to run. 59 | 60 | ## 4. Set up your environment variables 61 | 62 | Copy `.env.template` to a new file called `.env`. Then open up `.env` in your 63 | favorite code editor and fill out the values specified there. 64 | 65 | ``` 66 | cp .env.template .env 67 | ``` 68 | 69 | You can get your `PLAID_CLIENT_ID` and `PLAID_SECRET` values from the Keys 70 | section of the [Plaid dashboard](https://dashboard.plaid.com/developers/keys) 71 | 72 | Keep `sandbox` as your environment. 73 | 74 | You can probably keep `START_SYNC_NUM` as 0, unless your client is part of a 75 | team that has already been using Plaid Transfer extensively. 76 | 77 | **NOTE:** .env files are a convenient local development tool. Never run a 78 | production application using an environment file with secrets in it. Use some 79 | kind of Secrets Manager (provided by most commercial cloud providers) instead. 80 | 81 | ## 5. (Optional) Set up your webhook receiver 82 | 83 | Transfer makes use of webhooks to let applications know that the status of a 84 | payment has changed. If you want to see this part of the application in action, 85 | you will need to tell Plaid what webhook receiver it should send these messages 86 | to. 87 | 88 | ### Step 1: Create a public endpoint for your webhook receiver 89 | 90 | This webhook receiver will need to be available to the public in order for Plaid 91 | to communicate with it. If you don't wish to publish your sample application to 92 | a public server, one common option is to use a tool like 93 | [ngrok](https://ngrok.com/) to open up a tunnel from the outside world to a 94 | specific port running on `localhost`. 95 | 96 | The sample application uses a separate server to receive webhooks running on 97 | port 8001, so if you have ngrok installed, you can run 98 | 99 | ``` 100 | ngrok http 8001 101 | ``` 102 | 103 | to open up a tunnel from the outside world to this server. The final URL will be 104 | the domain that ngrok has created, plus the path `/server/receive_webhook`. It 105 | will probably look something like: 106 | 107 | `https://abde-123-4-567-8.ngrok.io/server/receive_webhook` 108 | 109 | ### Step 2: Add this URL to the .env file 110 | 111 | Normally, you would use the Webhooks section of the Plaid dashboard to tell 112 | Plaid what endpoint to call when a Transfer event happens. 113 | 114 | However, when you are running Transfer in the Sandbox environment, Transfer 115 | won't regularly send any webhooks. You'll need to tell Plaid, through its 116 | /sandbox/transfer/fire_webhook endpoint, to fire a webhook and to what URL. Our 117 | sample application grabs the URL to use from the SANDBOX_WEBHOOK_URL value in 118 | the .env file. 119 | 120 | ## 7. Run the application! 121 | 122 | You can run your application by typing 123 | 124 | ``` 125 | npm run watch 126 | ``` 127 | 128 | on the command line. If there are no issues, you should see a message telling 129 | you that you can open up http://localhost:8000/ to view your running app! 130 | 131 | # Running the application 132 | 133 | Pay Your Electric Bill is a fictional website that utilizes Plaid Transfer so 134 | that customers can use pay-by-bank to pay their electric bill. 135 | 136 | This sample application simulates two different ways that a user could use Plaid 137 | Transfer to pay by bank. Obviously, in a real app, you wouldn't use both 138 | options; this is just for demonstration purposes. 139 | 140 | Create a fictional customer account or sign in with a existing account to start 141 | the process. 142 | 143 | To create a bill, simply click the **Generate a new bill** button. One will be 144 | randomly generated for you. 145 | 146 | ## 1. Paying your bill with Transfer UI 147 | 148 | Transfer UI is a feature built into Link, the UI widget provided by Plaid. It 149 | takes care of connecting your user to a checking or savings account if 150 | necessary, and then properly collecting proof of authorization data to ensure 151 | you stay compliant with Nacha guidelines. 152 | 153 | Using Transfer UI doesn't require installing any additional libraries on the 154 | client -- it's already part of Link. 155 | 156 | To pay your bill using Tranfer UI, click the "Pay" link next to any individual 157 | bill. This will take you to a Bill Details page where you can see details about 158 | your bill, including the original amount, and how much is still due. 159 | 160 | From the Bill Details page, enter an amount to pay and click the **Pay Bill** 161 | button. 162 | 163 | If this is your first time using this application and you have not connected 164 | Plaid to any checking or savings accounts with this user, Plaid will prompt you 165 | to connect to a new bank. 166 | 167 | Go through the standard process for connecting to a new bank in Sandbox -- pick 168 | any institution you'd like, enter `user_good` and `pass_good` as the user name 169 | and password, and enter `1234` for an MFA code if prompted. 170 | 171 | Once you're done connecting to a bank, Plaid will then ask you to authorize a 172 | payment from the account you've just connected to. Click Accept, and your 173 | payment is submitted. 174 | 175 | To make subsequent payments with the same account, select the account you've 176 | previously connected, enter an amount, and click "Pay". Plaid will once again 177 | display the authorization form, and then submit your payment. 178 | 179 | ### How it works 180 | 181 | You should view the code for the complete details, but here's the brief summary 182 | of how Plaid Transfer works making use of Link's Transfer UI. 183 | 184 | #### Already connected an account? 185 | 186 | If your customer has made a payment in the past and has, therefore, already 187 | connected their bank to your application through Plaid, this is the overall 188 | process for making a payment: 189 | 190 | 1. When a user chooses to make a payment, the client calls the 191 | `/server/payments/initiate` endpoint on the locally-running server, passing 192 | along the account ID to use. 193 | 194 | 2. On the server, the application creates a Transfer Intent by making a call to 195 | Plaid's `/transfer/intent/create` endpoint. It includes data about the 196 | payment such as the user's legal name, the account they're using, the amount 197 | of the payment, and so on. It receives back an `intent_id`. 198 | 199 | 3. The server saves this payment information in its local database, storing the 200 | `intent_id` alongside the rest of the payment information. 201 | 202 | 4. The server then creates a link token through Plaid's `/link/token/create` 203 | call, sending the `intent_id` that it received in the previous step, along 204 | with a few new pieces of information (like the `products` array, the user's 205 | language and so on). It receives back a `link_token`, which can be used on 206 | the client to display a properly configured Link session. 207 | 208 | 5. This `link_token` is then returned to the client and the client uses the 209 | Plaid JavaScript SDK to open Link. 210 | 211 | 6. Inside of Link, the user is presented with a Nacha-compliant authorization 212 | form, so they can authorize the payment. Plaid stores this proof of 213 | authorization on its servers. 214 | 215 | 7. Once the user completes the Link process successfully, the payment is in 216 | Plaid's system and is ready to be sent off to the ACH network. We just need 217 | to make sure our application knows about the transfer that was created. 218 | 219 | 8. In Link's `onSuccess()` callback, the client sends down the original intent 220 | ID to the server's `/payments/transfer_ui_complete` endpoint. The server then 221 | calls `/transfer/intent/get` with this `intent_id` to get updated information 222 | about the transfer. 223 | 224 | Two important pieces of information received in the response are a) The 225 | `authorization_decision` and `authorization_decision_rationale`, which 226 | indicates if Plaid decided to approve or reject the transfer, and b) the 227 | `transfer_id` which is the ID of the transfer that was created by Plaid. This 228 | is different than the earlier `intent_id`, and will be used to identify this 229 | payment in Plaid's system from now on. 230 | 231 | 9. All of this information is saved in the database and is used to populate the 232 | "Payments for this bill" table. 233 | 234 | #### Need to connect an account? 235 | 236 | If your user has not yet connected their account with your application using 237 | Plaid (or they wish to connect a new account), the process works similar to 238 | before, but with these differences: 239 | 240 | 1. When the server calls `/transfer/intent/create`, the `account_id` field will 241 | be null because we don't have the `account_id` that will be used. This is a 242 | signal to Plaid that, when the user goes through the Link flow, Link needs to 243 | prompt them to connect to a financial institution. 244 | 245 | 2. Inside of Link, the user is first asked to connect to a checking our savings 246 | account before they are presented with the transfer authorization form. 247 | 248 | 3. When Link is complete, Plaid takes the `public_token` that it receives in the 249 | `onSuccess()` callback, and sends it down to its server to exchange for an 250 | access token, like you might do with any other Plaid product. This is bundled 251 | into the `/payments/transfer_ui_complete` call, rather than making two 252 | separate calls. 253 | 254 | 4. Because our application still wants to know what account was eventually used 255 | with the transfer, our server also makes a separate call to `/transfer/get`, 256 | to find out value of the `account_id` that was used in the transfer. 257 | 258 | ## 1a. Paying your bill without Transfer UI 259 | 260 | If you're interested in seeing the process for implementing pay-by-bank without 261 | using Link's Transfer UI feature, you can select the "Without Transfer UI" tab 262 | and follow the process there. 263 | 264 | The UI should look similar to the previous one -- the user can select an 265 | existing bank or ask to connect to a new one, then they can specify an amount 266 | and pay their bill. The biggest change you'll notice is that the confirmation 267 | dialog is supplied by the application, not Plaid. Behind the scenes, the 268 | endpoints used are different, and you as an application developer will need to 269 | perform additional work to store proof of authorization. 270 | 271 | ### How it works 272 | 273 | Again, you should view the code for the complete details, but here's the brief 274 | summary of how Transfer without using Link's Transfer UI works 275 | 276 | #### Already connected an account? 277 | 278 | If your customer has made a payment in the past and has, therefore, already 279 | connected their bank to your application through Plaid, this is the overall 280 | process for making a payment: 281 | 282 | 1. Our client displays a dialog to the user requesting their proof of 283 | authorization. This is just a placeholder dialog and should not be considered 284 | a definitive example. We recommend reading 285 | [Nacha's guidelines](https://www.nacha.org/system/files/2022-11/WEB_Proof_of_Authorization_Industry_Practices.pdf) 286 | for payments or working with your Plaid representative to make sure you 287 | display the proper authorization language. 288 | 289 | 2. After that, the client makes a separate call to the 290 | `/server/payments/no_transfer_ui/store_proof_of_authorization_necessary_for_nacha_compliance` 291 | endpoint. This is a dummy endpoint that demonstrates some of the data you 292 | would want to store for proof of authorization. You should store this data 293 | for at least two years, and may need to provide Plaid with this information 294 | if there is a customer dispute. Again, see 295 | [Nacha's guidelines](https://www.nacha.org/system/files/2022-11/WEB_Proof_of_Authorization_Industry_Practices.pdf) 296 | for additional information. 297 | 298 | 3. The client then makes a call to 299 | `/server/payments/no_transfer_ui/authorize_and_create` with information about 300 | the transfer. 301 | 302 | 4. The server first creates an entry in its database's `payments` table for this 303 | payment. We do this to create a unique ID that we can use as an idempotency 304 | key in the next step. 305 | 306 | 5. The server calls Plaid's `/transfer/authorization/create` endpoint to 307 | authorize this payment. This call checks, among other things, that the 308 | routing and account numbers are valid, and that the user has enough money in 309 | their account to avoid running into NSF (insufficient fund) errors. 310 | 311 | This endpoint will return a `decision` value of `declined` or `approved`, 312 | although Plaid will default to approving transfers if it doesn't have enough 313 | data otherwise. So if Plaid can't connect to a bank to see the user's 314 | available balance, it will be marked as `approved` and a note will be make in 315 | the `decision_rationale` field. You should check this field and determine the 316 | right course for your application. 317 | 318 | 6. Finally, the server makes a call to `/transfer/create`, passing along the 319 | `authorization_id` that was returned in the previous step, along with some 320 | additional information about that payment. 321 | 322 | 7. At this point, the payment is in Plaid's system and is ready to be sent off 323 | to the ACH network. All of this information is saved in the database and is 324 | used to populate the "Payments for this bill" table. 325 | 326 | #### Need to connect an account? 327 | 328 | If your user has not yet connected their account with your application using 329 | Plaid (or they wish to connect a new account), the process works similar to 330 | before, but with these differences: 331 | 332 | 1. When the user specifies that they wish to connect to a new account to make 333 | the payment, the client calls the `/server/token/create` endpoint on the 334 | locally-running server. 335 | 336 | 2. Our server generates a `link_token` by calling Plaid's `/link/token/create` 337 | endpoint, specifying `["transfer"]` as the list of products that are 338 | required. 339 | 340 | 3. The server sends this `link_token` up to the client, which then uses the 341 | Plaid JavaScript SDK to open Link. 342 | 343 | 4. If the user successfully completes the Link process, the client receives a 344 | `public_token`, which it then sends down to our server (via the 345 | `/server/tokens/exchange_public_token`), to exchange for a more permanent 346 | `access_token`. 347 | 348 | 5. This endpoint also accepts a `returnAccountId: true` argument, which it uses 349 | to send back an `account_d` belonging to the recently connected bank. This is 350 | how our client knows which bank account to use in the upcoming transfer. In a 351 | real application, you should be using a Link flow that requires the user to 352 | select only a single account so there's no risk of ambiguation here. 353 | 354 | 6. We then proceed with the "Already connected to an account" flow, using the 355 | `account_id` that we have retrieved in the previous step. 356 | 357 | ## 2. Updating payment statuses 358 | 359 | When you submit a payment, it will be marked as "Pending" in Plaid's system, 360 | meaning it's bundled up on Plaid's servers and ready to submit to the ACH 361 | network. In an actual application, it would be sent to the ACH network a couple 362 | of hours later, (where its status would change to "Posted") and then the payment 363 | will appear on the user's bank statement in a day or so (where its status would 364 | change to "Settled.") 365 | 366 | In the Sandbox environment, this doesn't happen automatically. You will need to 367 | change the status of the transfer manually. You can either do this by making 368 | calls to the `/sandbox/transfer/simulate` endpoint, or by using the UI within 369 | the Plaid Dashboard. 370 | 371 | This sample application uses the Plaid Dashboard. Next to every payment is a 372 | dashboard icon. Clicking this icon will take you to the Payment's appropriate 373 | entry in the Plaid dashboard. From there, you can click on "Next Event" to 374 | simulate the next event that normally takes place in the payment process. You 375 | can also click "Failed" to simulate when a transfer might fail (for instance, if 376 | you have an incorrect account or routing number) or "Return" to simulate when a 377 | transfer is returned (typically for insufficient funds, or if the user disputes 378 | a payment). 379 | 380 | ## 3. Receiving status updates 381 | 382 | When the user's payment's status has changed, our application will need to know 383 | about that. While the Plaid API contains several endpoints to fetch the status 384 | of individual payments, the recommended way of staying on top of all changes is 385 | to call `/transfer/event/sync` (with an `after_id` value). This will fetch a 386 | sequential list of transfer events since the `after_id` event. 387 | 388 | These events contain all of the information needed to stay on top of payment 389 | statuses. Most commonly, this will reflect the fact that a payment's status has 390 | changed. When a payment's status has changed, we record that information our 391 | database and update the total amount due associated with a bill. Payments that 392 | are marked as `settled`, for instance, can generally be considered to be 393 | completed and can be deducted from the total "amount due. However, the user can 394 | still dispute unauthorized charges for up to 60 days after the payment. We also 395 | display a "amount pending" value, which is the sum of the payments that are 396 | currently marked as "pending" or "posted". 397 | 398 | Our code contains some logic to ignore payments that follow "impossible" state 399 | logic (for example, if a payment were to go from `settled` to `pending`) This 400 | won't happen in Plaid's event sync logic, but it can happen during development. 401 | For instance, if you were to replay a batch of events you had already processed. 402 | Our code also ignores payments that it cant find in its database. That might 403 | happen if, say, multiple developers were running separate sample applications 404 | with the same Plaid client_id. 405 | 406 | In our application, our server calls `/transfer/event/sync` in response to 407 | clicking the "Perform Server Sync" logic on the client. In a real application, 408 | you may wish to make this call in response to receiving the 409 | `TRANSFER_EVENTS_UPDATE` webhook, which you will receive whenever the status of 410 | any event changes. Alternatively, you can simply call this endpoint on a 411 | regularly scheduled basis. 412 | 413 | #### Receiving webhooks 414 | 415 | In a normal Production environment, Plaid will automatically fire a 416 | `TRANSFER_EVENTS_UPDATE` webhook whenever the status of any transfer changes; 417 | the webhook contains no other information, only that the status of a transfer 418 | has changed. You may wish to have your application run `/transfer/event/sync` in 419 | response to receiving this webhook as a way of automatically staying up to date 420 | with any changes to your users' payments. 421 | 422 | In the Sandbox environment, however, Plaid does not automatically fire any 423 | webhooks. Your application will need to make a call to 424 | `/sandbox/transfer/fire_webhook`, which tells Plaid to sent a 425 | `TRANSFER_EVENTS_UPDATE` webhook to a URL that you pass in. 426 | 427 | In our application, clicking the "Fire a webhook" button will send a call to the 428 | `/server/debug/fire_webhook` endpoint on the server, which in turn will call 429 | Plaid's `/sandbox/transfer/fire_webhook` endpoint. This sends a webhook to the 430 | URL that you have specified in your .env file. 431 | 432 | If you have a working tunnel between this URL and your webhook receiver, this 433 | webhook should be picked up by the webhook server in `webhookServer.js`. If the 434 | server sees that this is a `TRANSFER_EVENTS_UPDATE` webhook, then it will call 435 | the internal `syncPaymentData()` function that calls `/transfer/events/sync` and 436 | processes the data. (This is the same function that is called by the "Perform 437 | Server Sync" button.) 438 | 439 | # What files do what? 440 | 441 | Here are a list of files in the application along with a brief description of 442 | what they do. Files in **bold** contain the code most relevant to implementing 443 | Plaid Transfer. 444 | 445 | ### Files on the server 446 | 447 | - `db.js` -- All the work for interacting with the database is performed here 448 | - `plaid.js` -- Initializes the Plaid client library 449 | - **`recalculateBills.js`** -- Calculates the status of a bill based on the 450 | status of all the associated payments in our database. Your application's 451 | logic may differ. 452 | - `server.js` -- Starts up the server and reads in all the routes listed below 453 | - **`syncPaymentData.js`** -- Calls `/transfer/event/sync` and updates the 454 | payment's status based on the events it receives 455 | - `types.js` -- A helper file that contains a couple of enum-like objects 456 | - `utils.js` -- Other utilities -- currently just used to get information about 457 | the signed in user 458 | - `webhookServer.js` -- A second server running on port 8001 to respond to 459 | webhooks 460 | - `/routes/banks.js` -- List banks and accounts that the user is connected to 461 | - `/routes/bills.js` -- List, generate and fetch details about bills 462 | - `/routes/debug.js` -- A place to put arbitrary rest calls 463 | - **`/routes/payments_no_transferUI.js`** -- Authorize a transfer, and create 464 | one _without_ using Link's Transfer UI. 465 | - **`/routes/payments.js`** -- List payments, create a Transfer Intent, and 466 | create a payment using Link's Transfer UI. 467 | - **`/routes/token.js`** -- Create a link token, exchange a public token for an 468 | access token, also does all the work around fetching and saving bank names 469 | - `/routes/user.js` -- Sign in, sign out, create user, etc. 470 | 471 | ### Files On the client 472 | 473 | - **`js/bill-details.js`** -- Does much more than get bill details! This 474 | performs the client logic necessary to pay bills, both with and without 475 | Transfer UI. We should probably rename or split up this file. 476 | - `js/client-bills.js` -- Fetches and displays info about the user's bills 477 | - `js/home.hs` -- Handle creating in and signing in users 478 | - `js/link.js` -- Initialize and run Link, send the public token down to the 479 | server 480 | - `js/signin.js` -- Gets users, signs in users, signs out users, and calls a 481 | "signedInUserCallback" or "signedOutUserCallback" depending on the user's 482 | status 483 | - `js/utils.js` -- Utilities, including the `callMyServer` method (which 484 | communicates with our server) and functions to display dates and currency in a 485 | user-friendly way. 486 | -------------------------------------------------------------------------------- /database/note.txt: -------------------------------------------------------------------------------- 1 | This folder is for our database that will be used to store users, accounts, and access tokens 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base-sample-app", 3 | "version": "1.0.0", 4 | "description": "Basic Sample Application for Plaid Products", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server/server.js", 8 | "watch": "nodemon server/server.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "css": "sass scss/custom.scss:public/css/custom_bootstrap.css" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.plaid.com/plaid/base-sample-app" 15 | }, 16 | "author": "Todd Kerpelman", 17 | "license": "ISC", 18 | "dependencies": { 19 | "body-parser": "^1.20.2", 20 | "bootstrap": "^5.3.2", 21 | "cookie-parser": "^1.4.6", 22 | "dotenv": "^16.3.1", 23 | "escape-html": "^1.0.3", 24 | "express": "^4.18.2", 25 | "moment": "^2.29.4", 26 | "nodemon": "^3.0.2", 27 | "plaid": "^22.0.1", 28 | "sqlite": "^5.1.1", 29 | "sqlite3": "^5.1.6", 30 | "uuid": "^9.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/bill-details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | View my Bills 12 | 13 | 14 | 15 |
16 |

Bill details

17 |

How'd you like to pay your bill using Plaid Transfer?

18 |
19 |

Let's pay your bill

20 | Back to bills 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 |
Bill
Original Amount
Amount Paid
Amount Pending
Remaining
46 |
47 |
48 |
49 | 59 | 60 |
61 |
62 |
63 | 65 | 66 |
67 |
68 | 70 | 71 |
72 |
73 | 74 |
75 | 76 |
77 |
78 | 79 |
80 |
81 | 83 | 84 |
85 |
86 | 88 | 89 |
90 |
91 |
92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
DateAmountStatus
(Hover for details)
Dashboard
110 |
111 |

Debug area:

112 | 113 | 114 |
115 |
116 | 117 | 118 |
119 | 120 | 154 |
155 |
156 | 157 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /public/bills.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | View my Bills 11 | 12 | 13 | 14 |
15 |

Pay Your Electric Bill!

16 |

How'd you like to pay your bill using Plaid Transfer?

17 |
18 |

View my bills!

19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
BillDateAmount DueStatus
32 |
33 |

Debug area:

34 | 36 |
37 |
38 | 39 | 40 |
41 |
42 |
43 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/css/custom_bootstrap.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["../../node_modules/bootstrap/scss/mixins/_banner.scss","../../node_modules/bootstrap/scss/_root.scss","../../node_modules/bootstrap/scss/vendor/_rfs.scss","../../node_modules/bootstrap/scss/mixins/_color-mode.scss","../../node_modules/bootstrap/scss/_reboot.scss","../../node_modules/bootstrap/scss/_variables.scss","../../node_modules/bootstrap/scss/mixins/_border-radius.scss","../../node_modules/bootstrap/scss/_type.scss","../../node_modules/bootstrap/scss/mixins/_lists.scss","../../node_modules/bootstrap/scss/_images.scss","../../node_modules/bootstrap/scss/mixins/_image.scss","../../node_modules/bootstrap/scss/_containers.scss","../../node_modules/bootstrap/scss/mixins/_container.scss","../../node_modules/bootstrap/scss/mixins/_breakpoints.scss","../../node_modules/bootstrap/scss/_grid.scss","../../node_modules/bootstrap/scss/mixins/_grid.scss","../../node_modules/bootstrap/scss/_tables.scss","../../node_modules/bootstrap/scss/mixins/_table-variants.scss","../../node_modules/bootstrap/scss/forms/_labels.scss","../../node_modules/bootstrap/scss/forms/_form-text.scss","../../node_modules/bootstrap/scss/forms/_form-control.scss","../../node_modules/bootstrap/scss/mixins/_transition.scss","../../node_modules/bootstrap/scss/mixins/_gradients.scss","../../node_modules/bootstrap/scss/forms/_form-select.scss","../../node_modules/bootstrap/scss/forms/_form-check.scss","../../scss/custom.scss","../../node_modules/bootstrap/scss/forms/_form-range.scss","../../node_modules/bootstrap/scss/forms/_floating-labels.scss","../../node_modules/bootstrap/scss/forms/_input-group.scss","../../node_modules/bootstrap/scss/mixins/_forms.scss","../../node_modules/bootstrap/scss/_buttons.scss","../../node_modules/bootstrap/scss/mixins/_buttons.scss","../../node_modules/bootstrap/scss/_transitions.scss","../../node_modules/bootstrap/scss/_dropdown.scss","../../node_modules/bootstrap/scss/mixins/_caret.scss","../../node_modules/bootstrap/scss/_button-group.scss","../../node_modules/bootstrap/scss/_nav.scss","../../node_modules/bootstrap/scss/_navbar.scss","../../node_modules/bootstrap/scss/_card.scss","../../node_modules/bootstrap/scss/_accordion.scss","../../node_modules/bootstrap/scss/_breadcrumb.scss","../../node_modules/bootstrap/scss/_pagination.scss","../../node_modules/bootstrap/scss/mixins/_pagination.scss","../../node_modules/bootstrap/scss/_badge.scss","../../node_modules/bootstrap/scss/_alert.scss","../../node_modules/bootstrap/scss/_progress.scss","../../node_modules/bootstrap/scss/_list-group.scss","../../node_modules/bootstrap/scss/_close.scss","../../node_modules/bootstrap/scss/_toasts.scss","../../node_modules/bootstrap/scss/_modal.scss","../../node_modules/bootstrap/scss/mixins/_backdrop.scss","../../node_modules/bootstrap/scss/_tooltip.scss","../../node_modules/bootstrap/scss/mixins/_reset-text.scss","../../node_modules/bootstrap/scss/_popover.scss","../../node_modules/bootstrap/scss/_carousel.scss","../../node_modules/bootstrap/scss/mixins/_clearfix.scss","../../node_modules/bootstrap/scss/_spinners.scss","../../node_modules/bootstrap/scss/_offcanvas.scss","../../node_modules/bootstrap/scss/_placeholders.scss","../../node_modules/bootstrap/scss/helpers/_color-bg.scss","../../node_modules/bootstrap/scss/helpers/_colored-links.scss","../../node_modules/bootstrap/scss/helpers/_focus-ring.scss","../../node_modules/bootstrap/scss/helpers/_icon-link.scss","../../node_modules/bootstrap/scss/helpers/_ratio.scss","../../node_modules/bootstrap/scss/helpers/_position.scss","../../node_modules/bootstrap/scss/helpers/_stacks.scss","../../node_modules/bootstrap/scss/helpers/_visually-hidden.scss","../../node_modules/bootstrap/scss/mixins/_visually-hidden.scss","../../node_modules/bootstrap/scss/helpers/_stretched-link.scss","../../node_modules/bootstrap/scss/helpers/_text-truncation.scss","../../node_modules/bootstrap/scss/mixins/_text-truncate.scss","../../node_modules/bootstrap/scss/helpers/_vr.scss","../../node_modules/bootstrap/scss/mixins/_utilities.scss","../../node_modules/bootstrap/scss/utilities/_api.scss"],"names":[],"mappings":"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E;EEyWJ;IFtMQ;;;AE+MN;EACE;;;AAOJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOE;;;AAGF;EACE;;;AASF;EACE;EACA;;;AAQF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA;EACE;;;AAKF;EACE;;;AAOF;EACE;EACA;;;AAKF;EACE;;;AAKF;EACE;;;AAOF;EACE;EACA;;;AAQF;EACE;;;AAQF;EACE;;;AGrkBF;ELmQM,WALI;EK5PR,aFwoB4B;;;AEnoB5B;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AK/OR;ECvDE;EACA;;;AD2DF;EC5DE;EACA;;;AD8DF;EACE;;AAEA;EACE,cFsoB0B;;;AE5nB9B;EL8MM,WALI;EKvMR;;;AAIF;EACE,eFiUO;EH1HH,WALI;;AK/LR;EACE;;;AAIJ;EACE;EACA,eFuTO;EH1HH,WALI;EKtLR,OFtFS;;AEwFT;EACE;;;AEhGJ;ECIE;EAGA;;;ADDF;EACE,SJ+jDkC;EI9jDlC,kBJ+jDkC;EI9jDlC;EHGE;EIRF;EAGA;;;ADcF;EAEE;;;AAGF;EACE;EACA;;;AAGF;EPyPM,WALI;EOlPR,OJkjDkC;;;AMplDlC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ECHA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACsDE;EF5CE;IACE,WNkee;;;AQvbnB;EF5CE;IACE,WNkee;;;AQvbnB;EF5CE;IACE,WNkee;;;AQvbnB;EF5CE;IACE,WNkee;;;AQvbnB;EF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;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;ADiJA;EACE;EACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AEnKN;EACE,ebu2BsC;;;Aa91BxC;EACE;EACA;EACA;EhB8QI,WALI;EgBrQR,ab+lB4B;;;Aa3lB9B;EACE;EACA;EhBoQI,WALI;;;AgB3PV;EACE;EACA;EhB8PI,WALI;;;AiBtRV;EACE,Yd+1BsC;EHrkBlC,WALI;EiBjRR,Od+1BsC;;;Aep2BxC;EACE;EACA;EACA;ElBwRI,WALI;EkBhRR,afkmB4B;EejmB5B,afymB4B;EexmB5B,Of43BsC;Ee33BtC;EACA,kBfq3BsC;Eep3BtC;EACA;EdGE;EeHE,YDMJ;;ACFI;EDhBN;ICiBQ;;;ADGN;EACE;;AAEA;EACE;;AAKJ;EACE,Ofs2BoC;Eer2BpC,kBfg2BoC;Ee/1BpC,cf82BoC;Ee72BpC;EAKE,YfkhBkB;;Ae9gBtB;EAME;EAMA;EAKA;;AAKF;EACE;EACA;;AAIF;EACE,Of40BoC;Ee10BpC;;AAQF;EAEE,kBf8yBoC;Ee3yBpC;;AAIF;EACE;EACA;EACA,mBforB0B;EenrB1B,OfsyBoC;EiBp4BtC,kBjBqiCgC;Eer8B9B;EACA;EACA;EACA;EACA,yBfgsB0B;Ee/rB1B;ECzFE,YD0FF;;ACtFE;ED0EJ;ICzEM;;;ADwFN;EACE,kBf47B8B;;;Aen7BlC;EACE;EACA;EACA;EACA;EACA,afwf4B;Eevf5B,Of2xBsC;Ee1xBtC;EACA;EACA;;AAEA;EACE;;AAGF;EAEE;EACA;;;AAWJ;EACE,Yf4wBsC;Ee3wBtC;ElByII,WALI;EIvQN;;AcuIF;EACE;EACA;EACA,mBfooB0B;;;AehoB9B;EACE,YfgwBsC;Ee/vBtC;ElB4HI,WALI;EIvQN;;AcoJF;EACE;EACA;EACA,mBf2nB0B;;;AennB5B;EACE,Yf6uBoC;;Ae1uBtC;EACE,Yf0uBoC;;AevuBtC;EACE,YfuuBoC;;;AeluBxC;EACE,OfquBsC;EepuBtC,Qf8tBsC;Ee7tBtC,SfilB4B;;Ae/kB5B;EACE;;AAGF;EACE;EdvLA;;Ac2LF;EACE;Ed5LA;;AcgMF;EAAoB,Qf8sBkB;;Ae7sBtC;EAAoB,Qf8sBkB;;;AkB75BxC;EACE;EAEA;EACA;EACA;ErBqRI,WALI;EqB7QR,alB+lB4B;EkB9lB5B,alBsmB4B;EkBrmB5B,OlBy3BsC;EkBx3BtC;EACA,kBlBk3BsC;EkBj3BtC;EACA;EACA,qBlB+9BkC;EkB99BlC,iBlB+9BkC;EkB99BlC;EjBHE;EeHE,YESJ;;AFLI;EEfN;IFgBQ;;;AEMN;EACE,clBs3BoC;EkBr3BpC;EAKE,YlBi+B4B;;AkB79BhC;EAEE,elB6uB0B;EkB5uB1B;;AAGF;EAEE,kBlBu1BoC;;AkBl1BtC;EACE;EACA;;;AAIJ;EACE,alBsuB4B;EkBruB5B,gBlBquB4B;EkBpuB5B,clBquB4B;EHlgBxB,WALI;EIvQN;;;AiB8CJ;EACE,alBkuB4B;EkBjuB5B,gBlBiuB4B;EkBhuB5B,clBiuB4B;EHtgBxB,WALI;EIvQN;;;AiBwDA;EACE;;;ACxEN;EACE;EACA,YnBq6BwC;EmBp6BxC,cnBq6BwC;EmBp6BxC,enBq6BwC;;AmBn6BxC;EACE;EACA;;;AAIJ;EACE,enB25BwC;EmB15BxC;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EAEA;EACA,OnB04BwC;EmBz4BxC,QnBy4BwC;EmBx4BxC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QnB24BwC;EmB14BxC;;AAGA;ElB3BE;;AkB+BF;EAEE,enBm4BsC;;AmBh4BxC;EACE,QnB03BsC;;AmBv3BxC;EACE,cnBs1BoC;EmBr1BpC;EACA,YnB8foB;;AmB3ftB;EACE,kBChEM;EDiEN,cCjEM;;ADmEN;EAII;;AAIJ;EAII;;AAKN;EACE,kBCrFM;EDsFN,cCtFM;ED2FJ;;AAIJ;EACE;EACA;EACA,SnBk2BuC;;AmB31BvC;EACE;EACA,SnBy1BqC;;;AmB30B3C;EACE,cnBo1BgC;;AmBl1BhC;EACE;EAEA,OnB80B8B;EmB70B9B;EACA;EACA;ElBjHA;EeHE,YGsHF;;AHlHE;EG0GJ;IHzGM;;;AGmHJ;EACE;;AAGF;EACE,qBnB60B4B;EmBx0B1B;;AAKN;EACE,enBwzB8B;EmBvzB9B;;AAEA;EACE;EACA;;;AAKN;EACE;EACA,cnBsyBgC;;;AmBnyBlC;EACE;EACA;EACA;;AAIE;EACE;EACA;EACA,SnBspBwB;;;AmB/oB1B;EACE;;;AEnLN;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAIA;EAA0B,YrB8gCa;;AqB7gCvC;EAA0B,YrB6gCa;;AqB1gCzC;EACE;;AAGF;EACE,OrB+/BuC;EqB9/BvC,QrB8/BuC;EqB7/BvC;EACA;EJ1BF,kBGFQ;EC8BN,QrB6/BuC;EC1gCvC;EeHE,YKmBF;;ALfE;EKMJ;ILLM;;;AKgBJ;EJjCF,kBjB8hCyC;;AqBx/BzC;EACE,OrBw+B8B;EqBv+B9B,QrBw+B8B;EqBv+B9B;EACA,QrBu+B8B;EqBt+B9B,kBrBu+B8B;EqBt+B9B;EpB7BA;;AoBkCF;EACE,OrBo+BuC;EqBn+BvC,QrBm+BuC;EqBl+BvC;EJpDF,kBGFQ;ECwDN,QrBm+BuC;EC1gCvC;EeHE,YK6CF;;ALzCE;EKiCJ;ILhCM;;;AK0CJ;EJ3DF,kBjB8hCyC;;AqB99BzC;EACE,OrB88B8B;EqB78B9B,QrB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oBALI;E4BhRR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;E5BsQI,WALI;E4B/PR;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;ExBjBE;EgBfF,kBQkCqB;ETtBjB,YSwBJ;;ATpBI;EShBN;ITiBQ;;;ASqBN;EACE;EAEA;EACA;;AAGF;EAEE;EACA;EACA;;AAGF;EACE;ERrDF,kBQsDuB;EACrB;EACA;EAKE;;AAIJ;EACE;EACA;EAKE;;AAIJ;EAKE;EACA;EAGA;;AAGA;EAKI;;AAKN;EAKI;;AAIJ;EAGE;EACA;EACA;EAEA;EACA;;;AAYF;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADkGA;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADkGA;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADkGA;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADkGA;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADkGA;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADkGA;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADkGA;EC/GA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD4HA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmGA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmGA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmGA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmGA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmGA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmGA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmGA;EChHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD+GF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,iBzB8QwC;;AyBpQxC;EACE;;AAGF;EACE;;;AAWJ;ECjJE;EACA;E7B8NI,oBALI;E6BvNR;;;ADkJF;ECrJE;EACA;E7B8NI,oBALI;E6BvNR;;;ACnEF;EXgBM,YWfJ;;AXmBI;EWpBN;IXqBQ;;;AWlBN;EACE;;;AAMF;EACE;;;AAIJ;EACE;EACA;EXDI,YWEJ;;AXEI;EWLN;IXMQ;;;AWDN;EACE;EACA;EXNE,YWOF;;AXHE;EWAJ;IXCM;;;;AYpBR;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAGF;EACE;;ACwBE;EACE;EACA,a7B6hBwB;E6B5hBxB,gB7B2hBwB;E6B1hBxB;EArCJ;EACA;EACA;EACA;;AA0DE;EACE;;;AD9CN;EAEE;EACA;EACA;EACA;EACA;E/BuQI,yBALI;E+BhQR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;E/B0OI,WALI;E+BnOR;EACA;EACA;EACA;EACA;EACA;E3BzCE;;A2B6CF;EACE;EACA;EACA;;;AAwBA;EACE;;AAEA;EACE;EACA;;;AAIJ;EACE;;AA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;EAEA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;;AAGE;EAEE;;AAIJ;EACE;;;AASJ;EACE,ahC8gCkC;EgC7gClC,gBhC6gCkC;EgC5gClC;;AAEA;AAAA;AAAA;EAGE;;;AAaJ;EACE;EACA;EAGA;;;AAIF;EACE;EnCyII,WALI;EmClIR;EACA;EACA;EACA;E/BxIE;EeHE,YgB6IJ;;AhBzII;EgBiIN;IhBhIQ;;;AgB0IN;EACE;;AAGF;EACE;EACA;EACA;;;AAMJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AxB1HE;EwBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IhB9NJ,YgBgOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AxB5LR;EwBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IhB9NJ,YgBgOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AxB5LR;EwBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IhB9NJ,YgBgOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AxB5LR;EwBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IhB9NJ,YgBgOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AxB5LR;EwBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IhB9NJ,YgBgOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AAtDR;EAEI;EACA;;AAEA;EACE;;AAEA;EACE;;AAGF;EACE;EACA;;AAIJ;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EhB9NJ,YgBgOI;;AAGA;EACE;;AAGF;EACE;EACA;EACA;EACA;;;AAiBZ;AAAA;EAGE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAME;EACE;;;ACzRN;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EhCjBE;;AgCqBF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EhCtBF;EACA;;AgCyBA;EACE;EhCbF;EACA;;AgCmBF;AAAA;EAEE;;;AAIJ;EAGE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAQA;EACE;;;AAQJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EhC7FE;;;AgCkGJ;EACE;EACA;EACA;EACA;;AAEA;EhCxGE;;;AgCkHJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EhC1IE;;;AgC8IJ;AAAA;AAAA;EAGE;;;AAGF;AAAA;EhC3II;EACA;;;AgC+IJ;AAAA;EhClII;EACA;;;AgC8IF;EACE;;AzB3HA;EyBuHJ;IAQI;IACA;;EAGA;IAEE;IACA;;EAEA;IACE;IACA;;EAKA;IhC3KJ;IACA;;EgC6KM;AAAA;IAGE;;EAEF;AAAA;IAGE;;EAIJ;IhC5KJ;IACA;;EgC8KM;AAAA;IAGE;;EAEF;AAAA;IAGE;;;;ACpOZ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;ErC4PI,WALI;EqCrPR;EACA;EACA;EACA;EjCrBE;EiCuBF;ElB1BI,YkB2BJ;;AlBvBI;EkBUN;IlBTQ;;;AkBwBN;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ElBjDE,YkBkDF;;AlB9CE;EkBqCJ;IlBpCM;;;AkBgDN;EACE;;AAGF;EACE;EACA;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EjC7DE;EACA;;AiC+DA;EjChEA;EACA;;AiCoEF;EACE;;AAIF;EjC5DE;EACA;;AiC+DE;EjChEF;EACA;;AiCoEA;EjCrEA;EACA;;;AiC0EJ;EACE;;;AASA;EACE;EACA;EjC9GA;;AiCiHA;EAAgB;;AAChB;EAAe;;AAIb;EjCtHF;;AiC6HA;EjC7HA;;;AiCqIA;EACE;EACA;;;AC1JN;EAEE;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EtC+QI,WALI;EsCxQR;EACA;ElCAE;;;AkCMF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;;;ACrCJ;EAEE;EACA;EvC4RI,2BALI;EuCrRR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EjCpBA;EACA;;;AiCuBF;EACE;EACA;EACA;EvCgQI,WALI;EuCzPR;EACA;EACA;EACA;EpBpBI,YoBqBJ;;ApBjBI;EoBQN;IpBPQ;;;AoBkBN;EACE;EACA;EAEA;EACA;;AAGF;EACE;EACA;EACA;EACA,SpC2uCgC;EoC1uChC;;AAGF;EAEE;EACA;EnBtDF,kBmBuDuB;EACrB;;AAGF;EAEE;EACA;EACA;EACA;;;AAKF;EACE,apC8sCgC;;AoCzsC9B;EnC9BF;EACA;;AmCmCE;EnClDF;EACA;;;AmCkEJ;EClGE;EACA;ExC0RI,2BALI;EwCnRR;;;ADmGF;ECtGE;EACA;ExC0RI,2BALI;EwCnRR;;;ACFF;EAEE;EACA;EzCuRI,sBALI;EyChRR;EACA;EACA;EAGA;EACA;EzC+QI,WALI;EyCxQR;EACA;EACA;EACA;EACA;EACA;ErCJE;;AqCSF;EACE;;;AAKJ;EACE;EACA;;;AChCF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EtCHE;;;AsCQJ;EAEE;;;AAIF;EACE,avC6kB4B;EuC5kB5B;;;AAQF;EACE,evCs+C8B;;AuCn+C9B;EACE;EACA;EACA;EACA;EACA;;;AAQF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AC5DF;EACE;IAAK,uBxCyhD2B;;;AwCphDpC;AAAA;EAGE;E3CkRI,yBALI;E2C3QR;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;E3CsQI,WALI;E2C/PR;EvCRE;;;AuCaJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ExBxBI,YwByBJ;;AxBrBI;EwBYN;IxBXQ;;;;AwBuBR;EvBAE;EuBEA;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;;AAGE;EAJJ;IAKM;;;;AC3DR;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EAGA;EACA;ExCXE;;;AwCeJ;EACE;EACA;;AAEA;EAEE;EACA;;;AASJ;EACE;EACA;EACA;;AAGA;EAEE;EACA;EACA;EACA;;AAGF;EACE;EACA;;;AAQJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;ExCvDE;EACA;;AwC0DF;ExC7CE;EACA;;AwCgDF;EAEE;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;;AAIF;EACE;;AAEA;EACE;EACA;;;AAaF;EACE;;AAGE;ExCvDJ;EAZA;;AwCwEI;ExCxEJ;EAYA;;AwCiEI;EACE;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AjCtFR;EiC8DA;IACE;;EAGE;IxCvDJ;IAZA;;EwCwEI;IxCxEJ;IAYA;;EwCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AjCtFR;EiC8DA;IACE;;EAGE;IxCvDJ;IAZA;;EwCwEI;IxCxEJ;IAYA;;EwCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AjCtFR;EiC8DA;IACE;;EAGE;IxCvDJ;IAZA;;EwCwEI;IxCxEJ;IAYA;;EwCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AjCtFR;EiC8DA;IACE;;EAGE;IxCvDJ;IAZA;;EwCwEI;IxCxEJ;IAYA;;EwCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AjCtFR;EiC8DA;IACE;;EAGE;IxCvDJ;IAZA;;EwCwEI;IxCxEJ;IAYA;;EwCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AAcZ;ExChJI;;AwCmJF;EACE;;AAEA;EACE;;;AAaJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AC5LJ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA,O1CqpD2B;E0CppD3B,Q1CopD2B;E0CnpD3B;EACA;EACA;EACA;EzCJE;EyCMF;;AAGA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EAEE;EACA;EACA;;;AAQJ;EAHE;;;AASE;EATF;;;ACjDF;EAEE;EACA;EACA;EACA;EACA;E9CyRI,sBALI;E8ClRR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;E9C2QI,WALI;E8CpQR;EACA;EACA;EACA;EACA;EACA;E1CRE;;A0CWF;EACE;;AAGF;EACE;;;AAIJ;EACE;EAEA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;E1ChCE;EACA;;A0CkCF;EACE;EACA;;;AAIJ;EACE;EACA;;;AC9DF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;;;AAOF;EACE;EACA;EACA;EAEA;;AAGA;E5B5CI,Y4B6CF;EACA,W5Ck8CgC;;AgB5+C9B;E4BwCJ;I5BvCM;;;A4B2CN;EACE,W5Cg8CgC;;A4C57ClC;EACE,W5C67CgC;;;A4Cz7CpC;EACE;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;E3CrFE;E2CyFF;;;AAIF;EAEE;EACA;EACA;EClHA;EACA;EACA;EACA,SDkH0B;ECjH1B;EACA;EACA,kBD+G4D;;AC5G5D;EAAS;;AACT;EAAS,SD2GiF;;;AAK5F;EACE;EACA;EACA;EACA;EACA;E3CrGE;EACA;;A2CuGF;EACE;EACA;;;AAKJ;EACE;EACA;;;AAKF;EACE;EAGA;EACA;;;AAIF;EACE;EACA;EACA;EACA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pBE;IACE;IACA;IACA;IACA;IACA;;;A5CeJ;E4CZE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;;A5CKJ;E4CFE;IACE;IACA;IACA;IACA;IACA;IACA;;;A5CJJ;E4COE;IAEE;;;A5CTJ;E4CYE;IAGE;;;A5C5BJ;E4C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A5CnCN;E4C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IpC5BA,YoC8BA;;;ApC1BA;EoCYJ;IpCXM;;;ARuDJ;E4C5BE;IACE;IACA;IACA;IACA;IACA;;;A5CuBJ;E4CpBE;IACE;IACA;IACA;IACA;IACA;;;A5CeJ;E4CZE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;;A5CKJ;E4CFE;IACE;IACA;IACA;IACA;IACA;IACA;;;A5CJJ;E4COE;IAEE;;;A5CTJ;E4CYE;IAGE;;;A5C5BJ;E4C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A5CnCN;E4C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IpC5BA,YoC8BA;;;ApC1BA;EoCYJ;IpCXM;;;ARuDJ;E4C5BE;IACE;IACA;IACA;IACA;IACA;;;A5CuBJ;E4CpBE;IACE;IACA;IACA;IACA;IACA;;;A5CeJ;E4CZE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;;A5CKJ;E4CFE;IACE;IACA;IACA;IACA;IACA;IACA;;;A5CJJ;E4COE;IAEE;;;A5CTJ;E4CYE;IAGE;;;A5C5BJ;E4C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A5CnCN;E4C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IpC5BA,YoC8BA;;;ApC1BA;EoCYJ;IpCXM;;;ARuDJ;E4C5BE;IACE;IACA;IACA;IACA;IACA;;;A5CuBJ;E4CpBE;IACE;IACA;IACA;IACA;IACA;;;A5CeJ;E4CZE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;;A5CKJ;E4CFE;IACE;IACA;IACA;IACA;IACA;IACA;;;A5CJJ;E4COE;IAEE;;;A5CTJ;E4CYE;IAGE;;;A5C5BJ;E4C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A5CnCN;E4C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IpC5BA,YoC8BA;;;ApC1BA;EoCYJ;IpCXM;;;ARuDJ;E4C5BE;IACE;IACA;IACA;IACA;IACA;;;A5CuBJ;E4CpBE;IACE;IACA;IACA;IACA;IACA;;;A5CeJ;E4CZE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;;A5CKJ;E4CFE;IACE;IACA;IACA;IACA;IACA;IACA;;;A5CJJ;E4COE;IAEE;;;A5CTJ;E4CYE;IAGE;;;A5C5BJ;E4C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;AA/ER;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EpC5BA,YoC8BA;;ApC1BA;EoCYJ;IpCXM;;;AoC2BF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EAEE;;AAGF;EAGE;;;AA2BR;EPpHE;EACA;EACA;EACA,S7C0mCkC;E6CzmClC;EACA;EACA,kB7CUS;;A6CPT;EAAS;;AACT;EAAS,S7Cm+CyB;;;AoDr3CpC;EACE;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AC7IF;EACE;EACA;EACA;EACA;EACA;EACA,SrDgzCkC;;AqD9yClC;EACE;EACA;;;AAKJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAKA;EACE;;;AAIJ;EACE;IACE,SrDmxCgC;;;AqD/wCpC;EACE;EACA;EACA;;;AAGF;EACE;IACE;;;AH9CF;EACE;EACA;EACA;;;AIHF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;ACFF;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AAOR;EACE;EACA;;AAGE;EAEE;EACA;;;AC1BN;EACE;EAEA;;;ACHF;EACE;EACA,KzD6c4B;EyD5c5B;EACA;EACA,uBzD2c4B;EyD1c5B;;AAEA;EACE;EACA,OzDuc0B;EyDtc1B,QzDsc0B;EyDrc1B;EzCIE,YyCHF;;AzCOE;EyCZJ;IzCaM;;;;AyCDJ;EACE;;;ACnBN;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAKF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;ACrBJ;EACE;EACA;EACA;EACA;EACA,S3DumCkC;;;A2DpmCpC;EACE;EACA;EACA;EACA;EACA,S3D+lCkC;;;A2DvlChC;EACE;EACA;EACA,S3DmlC8B;;;A2DhlChC;EACE;EACA;EACA,S3D6kC8B;;;AQ9iChC;EmDxCA;IACE;IACA;IACA,S3DmlC8B;;E2DhlChC;IACE;IACA;IACA,S3D6kC8B;;;AQ9iChC;EmDxCA;IACE;IACA;IACA,S3DmlC8B;;E2DhlChC;IACE;IACA;IACA,S3D6kC8B;;;AQ9iChC;EmDxCA;IACE;IACA;IACA,S3DmlC8B;;E2DhlChC;IACE;IACA;IACA,S3D6kC8B;;;AQ9iChC;EmDxCA;IACE;IACA;IACA,S3DmlC8B;;E2DhlChC;IACE;IACA;IACA,S3D6kC8B;;;AQ9iChC;EmDxCA;IACE;IACA;IACA,S3DmlC8B;;E2DhlChC;IACE;IACA;IACA,S3D6kC8B;;;A4D5mCpC;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;ACRF;AAAA;ECIE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;AAAA;EACE;;;ACdF;EACE;EACA;EACA;EACA;EACA;EACA,S/DgcsC;E+D/btC;;;ACRJ;ECAE;EACA;EACA;;;ACNF;EACE;EACA;EACA,OlEisB4B;EkEhsB5B;EACA;EACA,SlE2rB4B;;;AmE/nBtB;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAjBJ;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASF;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAjBJ;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASF;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;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AAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;A3DVR;E2DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A3DVR;E2DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A3DVR;E2DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A3DVR;E2DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A3DVR;E2DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;ACtDZ;ED+CQ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;ACnCZ;ED4BQ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI","file":"custom_bootstrap.css"} -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Base Sample App 15 | 16 | 17 | 18 |
19 |

Pay Your Electric Bills

20 |

Hi, there! It's your local utility. Please sign in to view and pay your bills

21 |
22 |
23 |

Create an account!

24 | 25 | 26 | 27 | 28 |
29 |
30 |

Sign in!

31 | Or, sign in as one of these users: 32 | 33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/js/bill-details.js: -------------------------------------------------------------------------------- 1 | import { refreshSignInStatus, signOut } from "./signin.js"; 2 | import { 3 | callMyServer, 4 | currencyAmount, 5 | snakeToEnglish, 6 | prettyDate, 7 | getDetailsAboutStatus, 8 | } from "./utils.js"; 9 | import { initiatePaymentWasClicked } from "./make-payment.js"; 10 | import { startPaymentNoTUIWasClicked, paymentDialogConfirmed } from "./make-payment-no-tui.js"; 11 | 12 | /** 13 | * Call the server to see what banks the user is connected to. 14 | */ 15 | export const getPaymentOptions = async () => { 16 | const accountSelect = document.querySelector("#selectAccount"); 17 | const accountSelectNoTUI = document.querySelector("#selectAccountNoTUI"); 18 | const accountData = await callMyServer("/server/banks/accounts/list"); 19 | let innerHTML = ""; 20 | if (accountData == null || accountData.length === 0) { 21 | innerHTML = ``; 22 | } else { 23 | const bankOptions = accountData.map( 24 | (account) => 25 | `` 26 | ); 27 | innerHTML = 28 | bankOptions.join("\n") + 29 | ``; 30 | } 31 | accountSelect.innerHTML = innerHTML; 32 | accountSelectNoTUI.innerHTML = innerHTML; 33 | }; 34 | 35 | 36 | 37 | /** 38 | * Retrieve the list of payments for a bill and update the table. 39 | */ 40 | export const paymentsRefresh = async (billId) => { 41 | // Retrieve our list of payments 42 | const billsJSON = await callMyServer("/server/payments/list", true, { 43 | billId, 44 | }); 45 | const accountTable = document.querySelector("#reportTable"); 46 | if (billsJSON == null || billsJSON.length === 0) { 47 | accountTable.innerHTML = `No payments yet.`; 48 | return; 49 | } else { 50 | accountTable.innerHTML = billsJSON 51 | .map( 52 | (payment) => 53 | ` 54 | ${prettyDate(payment.created_date)} 55 | ${currencyAmount( 56 | payment.amount_cents / 100, 57 | "USD" 58 | )} 59 | ${snakeToEnglish(payment.status)} 63 | ` 65 | ) 66 | .join("\n"); 67 | } 68 | enableTooltips(); 69 | }; 70 | 71 | /** 72 | * Retrieve the details of the current bill and update the interface 73 | */ 74 | export const getBillDetails = async () => { 75 | console.log("Getting bill details"); 76 | // Grab the bill ID from the url argument 77 | const urlParams = new URLSearchParams(window.location.search); 78 | const billId = urlParams.get("billId"); 79 | if (billId == null) { 80 | window.location.href = "/client-bills.html"; 81 | } 82 | // Retrieve our bill details and update our site 83 | const billJSON = await callMyServer("/server/bills/get", true, { billId }); 84 | document.querySelector("#billDescription").textContent = billJSON.description; 85 | // Would you normally break this out in a customer's bill? Probably not. 86 | document.querySelector("#originalAmount").textContent = currencyAmount( 87 | billJSON.original_amount_cents / 100, 88 | "USD" 89 | ); 90 | document.querySelector("#amountPaid").textContent = currencyAmount( 91 | billJSON.paid_total_cents / 100, 92 | "USD" 93 | ); 94 | document.querySelector("#amountPending").textContent = currencyAmount( 95 | billJSON.pending_total_cents / 100, 96 | "USD" 97 | ); 98 | document.querySelector("#amountRemaining").textContent = currencyAmount( 99 | (billJSON.original_amount_cents - 100 | billJSON.pending_total_cents - 101 | billJSON.paid_total_cents) / 102 | 100, 103 | "USD" 104 | ); 105 | // Refresh our payments 106 | await paymentsRefresh(billId); 107 | }; 108 | 109 | /** 110 | * Tell the server to refresh the payment data from Plaid 111 | */ 112 | const performServerSync = async () => { 113 | await callMyServer("/server/debug/sync_events", true); 114 | await getBillDetails(); 115 | }; 116 | 117 | /** 118 | * This will fire off a webhook which, if our webhook receiver is configured 119 | * correctly, will call the same syncPaymentData that gets called in the 120 | * /server/debug/sync_events endpoint. So the outcome will look similar to 121 | * clicking the "Sync" button, but it's a little closer to representing a real 122 | * world scenario. 123 | */ 124 | const fireTestWebhook = async () => { 125 | await callMyServer("/server/debug/fire_webhook", true); 126 | setTimeout(getBillDetails, 1500); 127 | }; 128 | 129 | /** 130 | * If we're signed out, we shouldn't be here. Go back to the home page. 131 | */ 132 | const signedOutCallBack = () => { 133 | window.location.href = "/index.html"; 134 | }; 135 | 136 | /** 137 | * If we're signed in, let's update the welcome message and get the bill details. 138 | */ 139 | const signedInCallBack = (userInfo) => { 140 | console.log(userInfo); 141 | document.querySelector("#welcomeMessage").textContent = 142 | `Hi there, ${ 143 | userInfo.firstName?.trim() || userInfo.lastName?.trim() 144 | ? `${userInfo.firstName} ${userInfo.lastName}` 145 | : "Test User" 146 | }! Let's pay your bill.`; 147 | getBillDetails(); 148 | getPaymentOptions(); 149 | }; 150 | 151 | /** 152 | * Connects the buttons on the page to the functions above. 153 | */ 154 | const selectorsAndFunctions = { 155 | "#signOut": () => signOut(signedOutCallBack), 156 | "#payBill": initiatePaymentWasClicked, 157 | "#syncServer": performServerSync, 158 | "#fireWebhook": fireTestWebhook, 159 | "#payBillNoTUI": startPaymentNoTUIWasClicked, 160 | "#dlogConfirmBtn": paymentDialogConfirmed, 161 | }; 162 | 163 | Object.entries(selectorsAndFunctions).forEach(([sel, fun]) => { 164 | if (document.querySelector(sel) == null) { 165 | console.warn(`Hmm... couldn't find ${sel}`); 166 | } else { 167 | document.querySelector(sel)?.addEventListener("click", fun); 168 | } 169 | }); 170 | 171 | 172 | /** 173 | * Enable Bootstrap tooltips 174 | */ 175 | const enableTooltips = () => { 176 | const tooltipTriggerList = [].slice.call( 177 | document.querySelectorAll('[data-bs-toggle="tooltip"]') 178 | ); 179 | tooltipTriggerList.map(function (tooltipTriggerEl) { 180 | return new bootstrap.Tooltip(tooltipTriggerEl); 181 | }); 182 | }; 183 | 184 | await refreshSignInStatus(signedInCallBack, signedOutCallBack); 185 | -------------------------------------------------------------------------------- /public/js/client-bills.js: -------------------------------------------------------------------------------- 1 | import { refreshSignInStatus, signOut } from "./signin.js"; 2 | import { 3 | callMyServer, 4 | currencyAmount, 5 | capitalizeEveryWord, 6 | prettyDate, 7 | snakeToEnglish, 8 | } from "./utils.js"; 9 | 10 | /** 11 | * Create a new bill for the user. 12 | */ 13 | const createNewBill = async () => { 14 | await callMyServer("/server/bills/create", true); 15 | await billsRefresh(); 16 | }; 17 | 18 | /** 19 | * Grab the list of bills from the server and display them on the page 20 | */ 21 | export const billsRefresh = async () => { 22 | const billsJSON = await callMyServer("/server/bills/list"); 23 | // Let's add this to our table! 24 | const accountTable = document.querySelector("#reportTable"); 25 | if (billsJSON == null || billsJSON.length === 0) { 26 | accountTable.innerHTML = `No bills yet! Click the button below to create one!`; 27 | return; 28 | } 29 | 30 | accountTable.innerHTML = billsJSON 31 | .map((bill) => { 32 | const billActionLink = `${bill.status === "unpaid" ? "Pay" : "View" 33 | }`; 34 | return `${bill.description}${prettyDate( 35 | bill.created_date 36 | )}${currencyAmount( 37 | (bill.original_amount_cents - 38 | bill.paid_total_cents - 39 | bill.pending_total_cents) / 40 | 100, 41 | "USD" 42 | )}${snakeToEnglish( 43 | bill.status 44 | )}${billActionLink}`; 45 | }) 46 | .join("\n"); 47 | }; 48 | 49 | /** 50 | * If we're signed out, redirect to the home page 51 | */ 52 | const signedOutCallBack = () => { 53 | window.location.href = "/index.html"; 54 | }; 55 | 56 | /** 57 | * If we're signed in, update the welcome message and refresh the table of bills 58 | */ 59 | const signedInCallBack = (userInfo) => { 60 | console.log(userInfo); 61 | document.querySelector("#welcomeMessage").textContent = 62 | `Hi there, ${ 63 | userInfo.firstName?.trim() || userInfo.lastName?.trim() 64 | ? `${userInfo.firstName} ${userInfo.lastName}` 65 | : "Test User" 66 | }! Feel free to view or pay any of your bills.`; 67 | billsRefresh(); 68 | }; 69 | 70 | /** 71 | * Connects the buttons on the page to the functions above. 72 | */ 73 | const selectorsAndFunctions = { 74 | "#signOut": () => signOut(signedOutCallBack), 75 | "#newBill": createNewBill, 76 | }; 77 | 78 | Object.entries(selectorsAndFunctions).forEach(([sel, fun]) => { 79 | if (document.querySelector(sel) == null) { 80 | console.warn(`Hmm... couldn't find ${sel}`); 81 | } else { 82 | document.querySelector(sel)?.addEventListener("click", fun); 83 | } 84 | }); 85 | 86 | await refreshSignInStatus(signedInCallBack, signedOutCallBack); 87 | -------------------------------------------------------------------------------- /public/js/home.js: -------------------------------------------------------------------------------- 1 | import { createNewUser, refreshSignInStatus, signIn } from "./signin.js"; 2 | import { callMyServer, hideSelector, showSelector } from "./utils.js"; 3 | 4 | /** 5 | * Let's create a new account! (And then call the signedInCallback when we're done) 6 | */ 7 | export const createAccount = async function (signedInCallback) { 8 | const newUsername = document.querySelector("#username").value; 9 | const newFirstName = document.querySelector("#firstName").value; 10 | const newLastName = document.querySelector("#lastName").value; 11 | await createNewUser(signedInCallback, newUsername, newFirstName, newLastName); 12 | }; 13 | 14 | /** 15 | * Get a list of all of our users on the server. 16 | */ 17 | const getExistingUsers = async function () { 18 | const usersList = await callMyServer("/server/users/list"); 19 | if (usersList.length === 0) { 20 | hideSelector("#existingUsers"); 21 | } else { 22 | showSelector("#existingUsers"); 23 | document.querySelector("#existingUsersSelect").innerHTML = usersList.map( 24 | (userObj) => `` 25 | ); 26 | } 27 | }; 28 | 29 | 30 | /** 31 | * If we're signed out, show the welcome message and the sign-in options 32 | */ 33 | const signedOutCallback = () => { 34 | document.querySelector("#welcomeMessage").textContent = 35 | "Hi, there! It's your local utility. Please sign in to view and pay your bills"; 36 | getExistingUsers(); 37 | }; 38 | 39 | /** 40 | * If we're signed in, redirect to the bills page 41 | */ 42 | const signedInCallback = (userInfo) => { 43 | document.querySelector( 44 | "#welcomeMessage" 45 | ).textContent = `Hi there! So great to see you again! You are signed in as ${userInfo.username}!`; 46 | window.location.href = "/bills.html"; 47 | }; 48 | 49 | document.querySelector("#signIn")?.addEventListener("click", () => { 50 | signIn(signedInCallback); 51 | }); 52 | 53 | document.querySelector("#createAccount")?.addEventListener("click", () => { 54 | createAccount(signedInCallback); 55 | }); 56 | 57 | await refreshSignInStatus(signedInCallback, signedOutCallback); 58 | -------------------------------------------------------------------------------- /public/js/link.js: -------------------------------------------------------------------------------- 1 | import { callMyServer } from "./utils.js"; 2 | 3 | /** 4 | * Start Link and define the callbacks we will call if a user completes the 5 | * flow or exits early 6 | */ 7 | export const startLink = async function (linkToken, asyncCustomSuccessHandler) { 8 | const handler = Plaid.create({ 9 | token: linkToken, 10 | onSuccess: async (publicToken, metadata) => { 11 | console.log(`Finished with Link! ${JSON.stringify(metadata)}`); 12 | await asyncCustomSuccessHandler(publicToken, metadata); 13 | }, 14 | onExit: async (err, metadata) => { 15 | console.log( 16 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify( 17 | metadata 18 | )}` 19 | ); 20 | }, 21 | onEvent: (eventName, metadata) => { 22 | console.log(`Event ${eventName}, Metadata: ${JSON.stringify(metadata)}`); 23 | }, 24 | }); 25 | handler.open(); 26 | }; 27 | 28 | /** 29 | * This starts Link Embedded Institution Search (which we usually just call 30 | * Embedded Link) -- instead of initiating Link in a separate dialog box, we 31 | * start by displaying the "Search for your bank" content in the page itself, 32 | * then switch to the Link dialog after the user selects their bank. This 33 | * tends to increase uptake on pay-by-bank flows. 34 | * 35 | * If you don't want to use Embedded Link, you can always use the startLink 36 | * function instead to start link the traditional way. 37 | * 38 | */ 39 | export const startEmbeddedLink = async function (linkToken, asyncCustomSuccessHandler, targetDiv) { 40 | const handler = Plaid.createEmbedded({ 41 | token: linkToken, 42 | onSuccess: async (publicToken, metadata) => { 43 | console.log(`Finished with Link! ${JSON.stringify(metadata)}`); 44 | await asyncCustomSuccessHandler(publicToken, metadata); 45 | }, 46 | onExit: async (err, metadata) => { 47 | console.log( 48 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify( 49 | metadata 50 | )}` 51 | ); 52 | }, 53 | onEvent: (eventName, metadata) => { 54 | console.log(`Event ${eventName}, Metadata: ${JSON.stringify(metadata)}`); 55 | }, 56 | }, 57 | targetDiv); 58 | }; 59 | 60 | 61 | /** 62 | * Exchange our Link token data for an access token 63 | */ 64 | export const exchangePublicToken = async ( 65 | publicToken, 66 | getAccountId = false 67 | ) => { 68 | const { status, accountId } = await callMyServer( 69 | "/server/tokens/exchange_public_token", 70 | true, 71 | { 72 | publicToken: publicToken, 73 | returnAccountId: getAccountId, 74 | } 75 | ); 76 | console.log("Done exchanging our token."); 77 | return accountId; 78 | }; 79 | -------------------------------------------------------------------------------- /public/js/make-payment-no-tui.js: -------------------------------------------------------------------------------- 1 | 2 | import { startLink, exchangePublicToken, startEmbeddedLink } from "./link.js"; 3 | import { showSelector, hideSelector, callMyServer, currencyAmount, prettyDate } from "./utils.js"; 4 | import { getBillDetails, getPaymentOptions } from "./bill-details.js"; 5 | 6 | 7 | /************************ 8 | * 9 | * Not interested in using TransferUI? You would use these functions instead. 10 | * 11 | ***********************/ 12 | 13 | let pendingPaymentObject = {}; 14 | 15 | 16 | 17 | /** 18 | * We can show the embedded link UI if the user wants to connect to a new account. 19 | */ 20 | const shouldIShowEmbeddedLinkNoTUI = () => { 21 | if (document.querySelector("#selectAccountNoTUI").value === "new" && 22 | document.querySelector("#amountToPayNoTUI").value > 0) { 23 | showSelector("#plaidEmbedContainerNoTUI"); 24 | hideSelector("#payBillNoTUI"); 25 | addNewAccountThenStartPayment(); 26 | } else { 27 | hideSelector("#plaidEmbedContainerNoTUI"); 28 | showSelector("#payBillNoTUI"); 29 | } 30 | } 31 | 32 | /** 33 | * This function is called when the "Pay Bill" button is clicked. With the 34 | * embedded Link flow, this button is only displayed if you're asking to connect 35 | * to an existing account. So we can skip right to the "Show the payment 36 | * confirmation" dialog 37 | */ 38 | export const startPaymentNoTUIWasClicked = async () => { 39 | const accountId = document.querySelector("#selectAccountNoTUI").value; 40 | await preparePaymentDialog(accountId); 41 | }; 42 | 43 | 44 | /** 45 | * If our user decides to add a new account, we can create a link 46 | * token like normal. In the success handler, we ask our server to return an 47 | * account ID from the item that was just created, so we can then kick off the 48 | * actual payment. (Ideally, this works best if you've customized your link 49 | * flow so that your user selects a single account) 50 | */ 51 | const addNewAccountThenStartPayment = async () => { 52 | const linkTokenData = await callMyServer( 53 | "/server/tokens/create_link_token", 54 | true 55 | ); 56 | const successHandler = async (publicToken, metadata) => { 57 | console.log("Finished with Link!"); 58 | console.log(metadata); 59 | const newAccountId = await exchangePublicToken(publicToken, true); 60 | await getPaymentOptions(); 61 | // A little hacky, but let's change the value of our drop-down so we 62 | // can grab the account name later. 63 | document.querySelector("#selectAccountNoTUI").value = newAccountId; 64 | preparePaymentDialog(newAccountId); 65 | } 66 | 67 | // In the non-Transfer UI flow, Link is only used when we're connecting 68 | // to a new account. So we'll always use embedded Link here. 69 | const targetElement = document.querySelector("#plaidEmbedContainerNoTUI"); 70 | startEmbeddedLink(linkTokenData.link_token, successHandler, targetElement); 71 | }; 72 | 73 | /** 74 | * Next, we'll start a transfer by gathering up some information about the 75 | * payment and using that to populate a consent dialog. 76 | */ 77 | const preparePaymentDialog = async (accountId) => { 78 | const billId = new URLSearchParams(window.location.search).get("billId"); 79 | const amount = document.querySelector("#amountToPayNoTUI").value; 80 | console.log(`Paying bill ${billId} from bank ${accountId} for $${amount}`); 81 | if (billId == null || amount == null) { 82 | alert("Something went wrong"); 83 | return; 84 | } 85 | if (isNaN(amount) || amount <= 0) { 86 | alert("Please enter a valid amount."); 87 | return; 88 | } 89 | if (accountId === "new") { 90 | alert("Please select an account or click the 'Add new account' button."); 91 | return; 92 | } 93 | pendingPaymentObject = { billId, accountId, amount }; 94 | showDialog(amount); 95 | }; 96 | 97 | /** 98 | * And, here's the code to actually display the dialog. 99 | */ 100 | const showDialog = async (amount) => { 101 | // Set the title and message in the dialog 102 | document.querySelector( 103 | "#customDialogLabel" 104 | ).textContent = `Confirm your ${currencyAmount(amount, "USD")} payment`; 105 | document.querySelector("#dlogAccount").textContent = document.querySelector( 106 | "#selectAccountNoTUI" 107 | ).selectedOptions[0].textContent; 108 | 109 | document.querySelector("#dlogDate").textContent = prettyDate( 110 | new Date().toLocaleString() 111 | ); 112 | // Show the modal 113 | var myModal = new bootstrap.Modal(document.getElementById("customDialog"), { 114 | keyboard: false, 115 | }); 116 | myModal.show(); 117 | }; 118 | 119 | /** 120 | * If the user clicks "Confirm", we're going to store the proof of authorization 121 | * data necessary for Nacha compliance and then authorize and create the payment. 122 | * 123 | * You would do probably this in a single endpoint call, but we'm breaking it 124 | * out into two separate calls so you don't overlook this important step. 125 | */ 126 | export const paymentDialogConfirmed = async () => { 127 | await callMyServer( 128 | "/server/payments/no_transfer_ui/store_proof_of_authorization_necessary_for_nacha_compliance", 129 | true, 130 | { 131 | billId: pendingPaymentObject.billId, 132 | accountId: pendingPaymentObject.accountId, 133 | amount: pendingPaymentObject.amount, 134 | } 135 | ); 136 | const { status, message } = await callMyServer( 137 | "/server/payments/no_transfer_ui/authorize_and_create", 138 | true, 139 | { 140 | billId: pendingPaymentObject.billId, 141 | accountId: pendingPaymentObject.accountId, 142 | amount: pendingPaymentObject.amount, 143 | } 144 | ); 145 | if (status === "success") { 146 | await getBillDetails(); 147 | } else { 148 | alert(message); 149 | await getBillDetails(); 150 | } 151 | }; 152 | 153 | // Let's also set up the embedded link container logic 154 | document.querySelector("#selectAccountNoTUI").addEventListener("change", shouldIShowEmbeddedLinkNoTUI); 155 | document.querySelector("#amountToPayNoTUI").addEventListener("change", shouldIShowEmbeddedLinkNoTUI); 156 | -------------------------------------------------------------------------------- /public/js/make-payment.js: -------------------------------------------------------------------------------- 1 | import { startLink, exchangePublicToken, startEmbeddedLink } from "./link.js"; 2 | import { showSelector, hideSelector, callMyServer } from "./utils.js"; 3 | import { getBillDetails, getPaymentOptions } from "./bill-details.js"; 4 | 5 | /************************ 6 | * If you're using TransferUI (which we recommend for most developers getting started) 7 | * you would follow this approach. 8 | ***********************/ 9 | 10 | /** 11 | * We can show the embedded link UI if you want to connect to a new account. 12 | * Since the TransferUI flow requires defining the transfer amount as well, 13 | * we won't show the embedded link UI until the has also entered an amount. 14 | */ 15 | const shouldIShowEmbeddedLink = () => { 16 | if (document.querySelector("#selectAccount").value === "new" && 17 | document.querySelector("#amountToPay").value > 0) { 18 | showSelector("#plaidEmbedContainer"); 19 | hideSelector("#payBill"); 20 | initiatePayment(true); 21 | } else { 22 | hideSelector("#plaidEmbedContainer"); 23 | showSelector("#payBill"); 24 | } 25 | } 26 | 27 | /** 28 | * The button that initiates this event will only appear if the user has 29 | * selected a pre-existing account (and entered an amount to pay). So we 30 | * will skip the embedded Link portion and just bring up the Transfer UI "confirm" 31 | * dialog. 32 | */ 33 | export const initiatePaymentWasClicked = async (_) => { 34 | initiatePayment(false); 35 | } 36 | 37 | /** 38 | * We start by sending the payment information down to the server -- the server 39 | * will create a Transfer Intent, and then pass that intent over to 40 | * /link/token/create. So we end up with a Link token that we can use to 41 | * open Link and start the payment process. 42 | * 43 | * Note that if we don't send down an account ID to use, that's a sign to Link 44 | * that we'll need to connect to a bank first. 45 | */ 46 | export const initiatePayment = async (useEmbeddedSearch = false) => { 47 | console.log(`Starting payment, but embedded search is ${useEmbeddedSearch}`) 48 | const billId = new URLSearchParams(window.location.search).get("billId"); 49 | const accountId = document.querySelector("#selectAccount").value; 50 | const amount = document.querySelector("#amountToPay").value; 51 | console.log(`Paying bill ${billId} from bank ${accountId} for $${amount}`); 52 | if (billId == null || amount == null) { 53 | alert("Something went wrong"); 54 | return; 55 | } 56 | if (isNaN(amount) || amount <= 0) { 57 | alert("Please enter a valid amount."); 58 | return; 59 | } 60 | const { linkToken, transferIntentId } = await callMyServer( 61 | "/server/payments/initiate", 62 | true, 63 | { 64 | billId, 65 | accountId, 66 | amount, 67 | } 68 | ); 69 | 70 | // When we're all done, we're going to send the public token and the 71 | // original transfer intent ID back to the server so we can gather some 72 | // information about the payment that was just created. 73 | const successHandler = async (publicToken, _) => { 74 | console.log("Finished with Link!"); 75 | if (accountId === "new") { 76 | console.log( 77 | "Oh! Looks like you set up a new account. Let's exchange that token!" 78 | ); 79 | await exchangePublicToken(publicToken); 80 | } 81 | 82 | await callMyServer("/server/payments/transfer_ui_complete", true, { 83 | publicToken, 84 | transferIntentId, 85 | }); 86 | await Promise.all[(getBillDetails(), getPaymentOptions())]; 87 | }; 88 | if (useEmbeddedSearch) { 89 | const targetElement = document.querySelector("#plaidEmbedContainer"); 90 | startEmbeddedLink(linkToken, successHandler, targetElement); 91 | } else { 92 | startLink(linkToken, successHandler); 93 | } 94 | }; 95 | 96 | document.querySelector("#selectAccount").addEventListener("change", shouldIShowEmbeddedLink); 97 | document.querySelector("#amountToPay").addEventListener("change", shouldIShowEmbeddedLink); 98 | -------------------------------------------------------------------------------- /public/js/signin.js: -------------------------------------------------------------------------------- 1 | import { callMyServer } from "./utils.js"; 2 | 3 | const noop = () => { }; 4 | 5 | /** 6 | * Methods to handle signing in and signing out. Because this is just 7 | * a sample, we decided to skip the whole "creating a password" thing. 8 | */ 9 | 10 | /** 11 | * Create a new user and then call the signedInCallback when we're done. 12 | */ 13 | export const createNewUser = async function ( 14 | signedInCallback, 15 | newUsername, 16 | newFirstName, 17 | newLastName 18 | ) { 19 | await callMyServer("/server/users/create", true, { 20 | username: newUsername, 21 | firstName: newFirstName, 22 | lastName: newLastName, 23 | }); 24 | await refreshSignInStatus(signedInCallback, noop); 25 | }; 26 | 27 | /** 28 | * Sign the user in and then call our "refreshSignInStatus" method 29 | * with whatever callback function we passed in 30 | */ 31 | export const signIn = async function (signedInCallback) { 32 | const userId = document.querySelector("#existingUsersSelect").value; 33 | await callMyServer("/server/users/sign_in", true, { userId: userId }); 34 | await refreshSignInStatus(signedInCallback, noop); 35 | }; 36 | 37 | /** 38 | * Sign the user out and then call our "refreshSignInStatus" method 39 | * with whatever callback function we passed in 40 | */ 41 | export const signOut = async function (signedOutCallback) { 42 | await callMyServer("/server/users/sign_out", true); 43 | await refreshSignInStatus(noop, signedOutCallback); 44 | }; 45 | 46 | /** 47 | * This is typically called at the beginning of a page load to determine 48 | * what to do based on our user's sign-in status. There are two callbacks 49 | * that we pass in: one for when the user is signed in and one for when 50 | * the user is signed out. 51 | */ 52 | export const refreshSignInStatus = async function ( 53 | signedInCallback, 54 | signedOutCallback 55 | ) { 56 | const userInfoObj = await callMyServer("/server/users/get_my_info"); 57 | const userInfo = userInfoObj.userInfo; 58 | if (userInfo == null) { 59 | signedOutCallback(); 60 | } else { 61 | signedInCallback(userInfo); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /public/js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function to simplify the process of calling our server with a GET or POST 3 | * request. We also parse the JSON response and log it to the console, so 4 | * you can see what's happening. 5 | */ 6 | export const callMyServer = async function ( 7 | endpoint, 8 | isPost = false, 9 | postData = null 10 | ) { 11 | const optionsObj = isPost ? { method: "POST" } : {}; 12 | if (isPost && postData !== null) { 13 | optionsObj.headers = { "Content-type": "application/json" }; 14 | optionsObj.body = JSON.stringify(postData); 15 | } 16 | const response = await fetch(endpoint, optionsObj); 17 | if (response.status === 500) { 18 | await handleServerError(response); 19 | return; 20 | } 21 | const data = await response.json(); 22 | console.log(`Result from calling ${endpoint}: ${JSON.stringify(data)}`); 23 | return data; 24 | }; 25 | 26 | /** 27 | * Hide an item by their selector 28 | */ 29 | export const hideSelector = function (selector) { 30 | document.querySelector(selector).classList.add("d-none"); 31 | }; 32 | 33 | /** 34 | * Show an item by their selector 35 | */ 36 | export const showSelector = function (selector) { 37 | document.querySelector(selector).classList.remove("d-none"); 38 | }; 39 | 40 | /** 41 | * Print out a date in a user-friendly format 42 | */ 43 | export const prettyDate = function (isoString) { 44 | const date = new Date(isoString); 45 | 46 | const userFriendlyDateTime = date.toLocaleString("en-US", { 47 | weekday: "short", // "Tue" 48 | month: "short", // "Mar" 49 | day: "numeric", // "12" 50 | hour: "2-digit", // "02" 51 | minute: "2-digit", // "03" 52 | hour12: true, // Use AM/PM 53 | }); 54 | 55 | return userFriendlyDateTime; 56 | }; 57 | 58 | /** 59 | * Capitalize the first letter of every word in a string 60 | */ 61 | export const capitalizeEveryWord = function (str) { 62 | return str.replace(/\b[a-z]/g, function (char) { 63 | return char.toUpperCase(); 64 | }); 65 | }; 66 | 67 | /** 68 | * Convert a snake_case string to normal looking text 69 | */ 70 | export const snakeToEnglish = function (str) { 71 | return capitalizeEveryWord(str.replace(/_/g, " ")); 72 | }; 73 | 74 | 75 | /** 76 | * Display some text in our #debugOutput area 77 | */ 78 | export const showOutput = function (textToShow) { 79 | if (textToShow == null) return; 80 | const output = document.querySelector("#debugOutput"); 81 | output.textContent = textToShow; 82 | }; 83 | 84 | 85 | /** 86 | * Used to populate our tooltips 87 | */ 88 | export const getDetailsAboutStatus = function (status, failure_reason = "") { 89 | switch (status) { 90 | case "new": 91 | return "This payment row was created and there's probably a 'Pending' event waiting to be synced."; 92 | case "waiting_for_auth": 93 | return "Transfer finished before the auth step was complete. You may have quit the UI early, or authorization failed for some reason."; 94 | case "pending": 95 | return "This payment is bundled up at Plaid and is waiting to be sent to the ACH network."; 96 | case "posted": 97 | return "This payment has been sent to the ACH network and is typically settled within a day."; 98 | case "settled": 99 | return "The withdrawal has shown up on the user's bank statement. Funds will be placed in your Ledger's `pending` balance for 5 business days."; 100 | case "failed": 101 | return failure_reason; 102 | case "cancelled": 103 | return "This payment was cancelled by the user -- typically within a short window of sending it."; 104 | case "returned": 105 | return "The payment was returned. Possibly due to insufficient funds or the user disputed the charge."; 106 | default: 107 | return status; 108 | } 109 | }; 110 | 111 | const formatters = { 112 | USD: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }), 113 | }; 114 | 115 | /** 116 | * Display a number in a proper currency format 117 | */ 118 | export const currencyAmount = function (amount, currencyCode) { 119 | try { 120 | // Create a new formatter if this doesn't exist 121 | if (formatters[currencyCode] == null) { 122 | formatters[currencyCode] = new Intl.NumberFormat("en-US", { 123 | style: "currency", 124 | currency: currencyCode, 125 | }); 126 | } 127 | return formatters[currencyCode].format(amount); 128 | } catch (error) { 129 | console.log(error); 130 | return amount; 131 | } 132 | }; 133 | 134 | /** 135 | * Did we get an error from the server? Let's handle it. 136 | */ 137 | const handleServerError = async function (responseObject) { 138 | const error = await responseObject.json(); 139 | console.error("I received an error ", error); 140 | if (error.hasOwnProperty("error_message")) { 141 | showOutput(`Error: ${error.error_message} -- See console for more`); 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /scss/custom.scss: -------------------------------------------------------------------------------- 1 | $body-bg: #ffffff; 2 | $body-color: #344966; 3 | $primary: #357cda; 4 | $success: #bfcc94; 5 | $danger: #eca18a; 6 | $warning: #f9e5b8; 7 | $info: #3068d7; 8 | $dark: #0d1821; 9 | 10 | @import "../node_modules/bootstrap/scss/bootstrap"; 11 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const sqlite3 = require("sqlite3").verbose(); 3 | const dbWrapper = require("sqlite"); 4 | const { v4: uuidv4 } = require("uuid"); 5 | const { PAYMENT_STATUS } = require("./types"); 6 | 7 | // You may want to have this point to different databases based on your environment 8 | const databaseFile = "./database/appdata.db"; 9 | let db; 10 | 11 | // Set up our database 12 | const existingDatabase = fs.existsSync(databaseFile); 13 | 14 | const createUsersTableSQL = 15 | "CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL)"; 16 | const createItemsTableSQL = 17 | "CREATE TABLE items (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, " + 18 | "access_token TEXT NOT NULL, bank_name TEXT, " + 19 | "is_active INTEGER NOT_NULL DEFAULT 1, " + 20 | "created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + 21 | "FOREIGN KEY(user_id) REFERENCES users(id))"; 22 | const createAccountsTableSQL = 23 | "CREATE TABLE accounts (id TEXT PRIMARY KEY, item_id TEXT NOT NULL, " + 24 | "name TEXT, cached_balance FLOAT, FOREIGN KEY(item_id) REFERENCES items(id))"; 25 | const createBillsTableSQL = 26 | "CREATE TABLE bills (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, " + 27 | "created_date TEXT, description TEXT, original_amount_cents INT, paid_total_cents INT DEFAULT 0, " + 28 | "pending_total_cents INT DEFAULT 0, status TEXT, " + 29 | "FOREIGN KEY(user_id) REFERENCES users(id))"; 30 | const createPaymentsTableSQL = `CREATE TABLE payments ( 31 | id TEXT PRIMARY KEY, 32 | plaid_intent_id TEXT, 33 | plaid_id TEXT, 34 | plaid_auth_id TEXT, 35 | user_id TEXT NOT NULL, 36 | bill_id TEXT NOT NULL, 37 | account_id TEXT, 38 | amount_cents INT, 39 | authorized_status TEXT, 40 | auth_reason TEXT, 41 | failure_reason TEXT, 42 | status TEXT, 43 | created_date TEXT, 44 | FOREIGN KEY(user_id) REFERENCES users(id), 45 | FOREIGN KEY(bill_id) REFERENCES bills(id), 46 | FOREIGN KEY(account_id) REFERENCES accounts(id) 47 | )`; 48 | 49 | // A simple key-value store for app data -- we only use this to store the 50 | // "last sync event processed" number 51 | const createAppTableSQL = `CREATE TABLE appdata ( 52 | key TEXT PRIMARY KEY, 53 | value TEXT 54 | )`; 55 | 56 | dbWrapper 57 | .open({ filename: databaseFile, driver: sqlite3.Database }) 58 | .then(async (dBase) => { 59 | db = dBase; 60 | try { 61 | if (!existingDatabase) { 62 | // Database doesn't exist yet -- let's create it! 63 | await db.run(createUsersTableSQL); 64 | await db.run(createItemsTableSQL); 65 | await db.run(createAccountsTableSQL); 66 | await db.run(createBillsTableSQL); 67 | await db.run(createPaymentsTableSQL); 68 | await db.run(createAppTableSQL); 69 | } else { 70 | // Works around the rare instance where a database gets created, but the tables don't 71 | const tableNames = await db.all( 72 | "SELECT name FROM sqlite_master WHERE type='table'" 73 | ); 74 | const tableNamesToCreationSQL = { 75 | users: createUsersTableSQL, 76 | items: createItemsTableSQL, 77 | accounts: createAccountsTableSQL, 78 | bills: createBillsTableSQL, 79 | payments: createPaymentsTableSQL, 80 | appdata: createAppTableSQL, 81 | }; 82 | for (const [tableName, creationSQL] of Object.entries( 83 | tableNamesToCreationSQL 84 | )) { 85 | if (!tableNames.some((table) => table.name === tableName)) { 86 | console.log(`Creating ${tableName} table`); 87 | await db.run(creationSQL); 88 | } 89 | } 90 | console.log("Database is up and running!"); 91 | sqlite3.verbose(); 92 | } 93 | } catch (dbError) { 94 | console.error(dbError); 95 | } 96 | }); 97 | 98 | // Helper function that exposes the db if you wan to run SQL on it 99 | // directly. Only recommended for debugging. 100 | const debugExposeDb = function () { 101 | return db; 102 | }; 103 | 104 | /*********************************************** 105 | * Functions related to fetching or adding items 106 | * and accounts for a user 107 | * **********************************************/ 108 | 109 | const getItemsAndAccountsForUser = async function (userId) { 110 | try { 111 | const items = await db.all( 112 | `SELECT items.bank_name, accounts.id as account_id, accounts.name as account_name, accounts.cached_balance as balance 113 | FROM items JOIN accounts ON items.id = accounts.item_id 114 | WHERE items.user_id=? AND items.is_active = 1`, 115 | userId 116 | ); 117 | return items; 118 | } catch (error) { 119 | console.error(`Error getting items and accounts for user ${error}`); 120 | throw error; 121 | } 122 | }; 123 | 124 | const getItemInfoForAccountAndUser = async function (accountId, userId) { 125 | try { 126 | const item = await db.get( 127 | `SELECT items.id, items.access_token, items.bank_name, items.created_time 128 | FROM items JOIN accounts ON items.id = accounts.item_id 129 | WHERE accounts.id = ? AND items.user_id = ?`, 130 | accountId, 131 | userId 132 | ); 133 | return item; 134 | } catch (error) { 135 | console.error(`Error getting item for account ${error}`); 136 | throw error; 137 | } 138 | }; 139 | 140 | const getAccessTokenForUserAndAccount = async function (userId, accountId) { 141 | try { 142 | const item = await db.get( 143 | `SELECT items.access_token 144 | FROM items JOIN accounts ON items.id = accounts.item_id 145 | WHERE accounts.id = ? and items.user_id = ?`, 146 | accountId, 147 | userId 148 | ); 149 | 150 | return item.access_token; 151 | } catch (error) { 152 | console.error(`Error getting access token for user and account ${error}`); 153 | throw error; 154 | } 155 | }; 156 | 157 | const getAccessTokenForUserAndItem = async function (userId, itemId) { 158 | try { 159 | const item = await db.get( 160 | `SELECT id, access_token FROM items WHERE id = ? and user_id = ?`, 161 | itemId, 162 | userId 163 | ); 164 | 165 | return item.access_token; 166 | } catch (error) { 167 | console.error(`Error getting access token for user and item ${error}`); 168 | throw error; 169 | } 170 | }; 171 | 172 | const addItem = async function (itemId, userId, accessToken) { 173 | try { 174 | const result = await db.run( 175 | `INSERT INTO items(id, user_id, access_token) VALUES(?, ?, ?)`, 176 | itemId, 177 | userId, 178 | accessToken 179 | ); 180 | return result; 181 | } catch (error) { 182 | console.error(`Error adding item ${error}`); 183 | throw error; 184 | } 185 | }; 186 | 187 | const addBankNameForItem = async function (itemId, institutionName) { 188 | try { 189 | const result = await db.run( 190 | `UPDATE items SET bank_name=? WHERE id =?`, 191 | institutionName, 192 | itemId 193 | ); 194 | return result; 195 | } catch (error) { 196 | console.error(`Error adding bank name for item ${error}`); 197 | throw error; 198 | } 199 | }; 200 | 201 | const addAccount = async function (accountId, itemId, acctName, balance) { 202 | try { 203 | await db.run( 204 | `INSERT OR IGNORE INTO accounts(id, item_id, name, cached_balance) VALUES(?, ?, ?, ?)`, 205 | accountId, 206 | itemId, 207 | acctName, 208 | balance 209 | ); 210 | } catch (error) { 211 | console.error(`Error adding account ${error}`); 212 | throw error; 213 | } 214 | }; 215 | 216 | /*********************************************** 217 | * Functions related to Users 218 | * **********************************************/ 219 | 220 | const addUser = async function (userId, username, firstName, lastName) { 221 | try { 222 | const result = await db.run( 223 | `INSERT INTO users(id, username, first_name, last_name) VALUES(?, ?, ?, ?)`, 224 | userId, 225 | username, 226 | firstName, 227 | lastName 228 | ); 229 | return result; 230 | } catch (error) { 231 | console.error(`Error adding user ${error}`); 232 | throw error; 233 | } 234 | }; 235 | 236 | const getUserList = async function () { 237 | try { 238 | const result = await db.all(`SELECT id, username FROM users`); 239 | return result; 240 | } catch (error) { 241 | console.error(`Error getting user list ${error}`); 242 | throw error; 243 | } 244 | }; 245 | 246 | const getUserRecord = async function (userId) { 247 | try { 248 | const result = await db.get(`SELECT * FROM users WHERE id=?`, userId); 249 | return result; 250 | } catch (error) { 251 | console.error(`Error getting user record ${error}`); 252 | throw error; 253 | } 254 | }; 255 | 256 | const getBankNamesForUser = async function (userId) { 257 | try { 258 | const result = await db.all( 259 | `SELECT id, bank_name 260 | FROM items WHERE user_id=? AND is_active = 1`, 261 | userId 262 | ); 263 | return result; 264 | } catch (error) { 265 | console.error(`Error getting bank names for user ${error}`); 266 | throw error; 267 | } 268 | }; 269 | 270 | /*********************************************** 271 | * Functions related to Bills 272 | **********************************************/ 273 | const createNewBill = async function (userId) { 274 | try { 275 | const billId = uuidv4(); 276 | const someRandomDescriptions = [ 277 | "Monthly Electric Charge", 278 | "This Month's Sparky Bill", 279 | "Electricity Usage Invoice", 280 | "Watt's Up for This Month!", 281 | "Power Bill Snapshot", 282 | "Charge It Up - Monthly Bill", 283 | "Electric Bill Alert", 284 | "Power Play for the Month", 285 | "Monthly kWh Tally", 286 | "Lightning in a Bill!", 287 | ]; 288 | 289 | const amountDue = Math.floor(Math.random() * 15000 + 1); 290 | const description = 291 | someRandomDescriptions[ 292 | Math.floor(Math.random() * someRandomDescriptions.length) 293 | ]; 294 | 295 | const _ = await db.run( 296 | `INSERT INTO bills(id, user_id, created_date, description, original_amount_cents, paid_total_cents, pending_total_cents, status) VALUES(?, ?, ?, ?, ?, ?, ?, ?)`, 297 | billId, 298 | userId, 299 | new Date().toISOString(), 300 | description, 301 | amountDue, 302 | 0, 303 | 0, 304 | "unpaid" 305 | ); 306 | return billId; 307 | } catch (error) { 308 | console.error(`Error creating bill ${error}`); 309 | throw error; 310 | } 311 | }; 312 | 313 | const getBillsForUser = async function (userId) { 314 | try { 315 | const result = await db.all( 316 | `SELECT * from bills WHERE user_id = ?`, 317 | userId 318 | ); 319 | return result; 320 | } catch (error) { 321 | console.error(`Error getting bills for user ${error}`); 322 | throw error; 323 | } 324 | }; 325 | 326 | const getBillDetailsForUser = async function (userId, billId) { 327 | try { 328 | const result = await db.get( 329 | `SELECT * from bills WHERE user_id = ? AND id = ?`, 330 | userId, 331 | billId 332 | ); 333 | return result; 334 | } catch (error) { 335 | console.error(`Error getting bill details for user ${error}`); 336 | throw error; 337 | } 338 | }; 339 | 340 | /*********************************************** 341 | * Functions related to Payments 342 | * **********************************************/ 343 | 344 | const createPaymentForUser = async function ( 345 | userId, 346 | billId, 347 | transferId, 348 | accountId, 349 | amount_cents 350 | ) { 351 | try { 352 | // Our user is kicking off a payment, so let's record those details in the database. 353 | // We won't store the account ID yet, becuase we might not know it. 354 | const paymentId = uuidv4(); 355 | const _ = await db.run( 356 | `INSERT INTO payments(id, user_id, bill_id, plaid_intent_id, account_id, amount_cents, status, created_date) VALUES(?, ?, ?, ?, ?, ?, ?, ?)`, 357 | paymentId, 358 | userId, 359 | billId, 360 | transferId, 361 | accountId, 362 | amount_cents, 363 | "waiting_for_auth", 364 | new Date().toISOString() 365 | ); 366 | return paymentId; 367 | } catch (error) { 368 | console.error(`Error creating payment ${error}`); 369 | throw error; 370 | } 371 | }; 372 | 373 | const updatePaymentWithTransferIntent = async function ( 374 | userId, 375 | transferIntentId, 376 | transferId, 377 | accountId, 378 | authorizationDecision, 379 | authorizationRationale, 380 | intentStatus 381 | ) { 382 | // From the user's perspective, has this payment attempt been successful? This involves both 383 | // whether the transfer intent succeeded, and if the authorization succeeded 384 | 385 | try { 386 | let paymentStatus = ""; 387 | if (intentStatus === "SUCCEEDED") { 388 | if (authorizationDecision === "APPROVED") { 389 | paymentStatus = PAYMENT_STATUS.NEW; 390 | // Approved decisions that come with a rationale still might require a 391 | // second look. Usually this is for banks where Plaid can't verify their 392 | // account balance. 393 | if (authorizationRationale != null) { 394 | console.warn( 395 | "You might want to handle this:", 396 | authorizationRationale 397 | ); 398 | } 399 | } else if (authorizationDecision === "DENIED") { 400 | paymentStatus = PAYMENT_STATUS.DENIED; 401 | } 402 | } else if (intentStatus === "FAILED") { 403 | paymentStatus = PAYMENT_STATUS.FAILED; 404 | } else if (intentStatus === "PENDING") { 405 | paymentStatus = PAYMENT_STATUS.INTENT_PENDING; 406 | } 407 | const _ = await db.run( 408 | `UPDATE payments SET plaid_id=?, account_id = ?, authorized_status=?, auth_reason=?, status=? WHERE user_id=? AND plaid_intent_id=?`, 409 | transferId, 410 | accountId, 411 | authorizationDecision, 412 | authorizationRationale, 413 | paymentStatus, 414 | userId, 415 | transferIntentId 416 | ); 417 | } catch (error) { 418 | console.error(`Error updating payment ${error}`); 419 | throw error; 420 | } 421 | }; 422 | 423 | const addPaymentAuthorization = async function ( 424 | paymentId, 425 | authId, 426 | authStatus, 427 | decisionRationale 428 | ) { 429 | try { 430 | const _ = await db.run( 431 | `UPDATE payments SET plaid_auth_id=?, authorized_status=?, auth_reason=? WHERE id=?`, 432 | authId, 433 | authStatus, 434 | decisionRationale, 435 | paymentId 436 | ); 437 | } catch (error) { 438 | console.error(`Error adding payment authorization ${error}`); 439 | throw error; 440 | } 441 | }; 442 | 443 | const updatePaymentWithTransferInfo = async function ( 444 | paymentId, 445 | transferId, 446 | status, 447 | failureReason 448 | ) { 449 | try { 450 | const _ = await db.run( 451 | `UPDATE payments SET plaid_id=?, status=?, failure_reason=? WHERE id=?`, 452 | transferId, 453 | status, 454 | failureReason, 455 | paymentId 456 | ); 457 | } catch (error) { 458 | console.error(`Error updating payment creation ${error}`); 459 | throw error; 460 | } 461 | }; 462 | 463 | const updatePaymentWithAccountId = async function ( 464 | userId, 465 | plaidTransferId, 466 | newAccountId 467 | ) { 468 | try { 469 | const _ = await db.run( 470 | `UPDATE payments SET account_id=? WHERE user_id=? AND plaid_id=?`, 471 | newAccountId, 472 | userId, 473 | plaidTransferId 474 | ); 475 | } catch (error) { 476 | console.error(`Error updating payment with account ID ${error}`); 477 | throw error; 478 | } 479 | }; 480 | 481 | const getPaymentByPlaidId = async function (plaidId) { 482 | try { 483 | const payment = await db.get( 484 | `SELECT * FROM payments WHERE plaid_id = ?`, 485 | plaidId 486 | ); 487 | return payment; 488 | } catch (error) { 489 | console.error(`Error getting payment by plaid ID ${error}`); 490 | throw error; 491 | } 492 | }; 493 | 494 | const getPaymentsForUserBill = async function (userId, billId) { 495 | try { 496 | const payments = await db.all( 497 | `SELECT * FROM payments WHERE user_id = ? AND bill_id = ?`, 498 | userId, 499 | billId 500 | ); 501 | return payments; 502 | } catch (error) { 503 | console.error(`Error getting payments for user and bill ${error}`); 504 | throw error; 505 | } 506 | }; 507 | 508 | const updatePaymentStatus = async ( 509 | paymentId, 510 | status, 511 | billId, 512 | optionalError 513 | ) => { 514 | try { 515 | const { recalculateBill } = require("./recalculateBills"); 516 | await db.run("BEGIN TRANSACTION"); 517 | const updatePaymentResult = await db.run( 518 | `UPDATE payments SET status=? WHERE id=?`, 519 | status, 520 | paymentId 521 | ); 522 | if (updatePaymentResult.changes < 1) { 523 | throw new Error(`Couldn't find payment with id ${paymentId}`); 524 | } 525 | if (optionalError) { 526 | await db.run( 527 | `UPDATE payments SET failure_reason=? WHERE id=?`, 528 | optionalError, 529 | paymentId 530 | ); 531 | } 532 | 533 | await recalculateBill(billId); 534 | // TODO: Recalculate the bill's status based on the payments 535 | await db.run("COMMIT"); 536 | } catch (error) { 537 | await db.run("ROLLBACK"); 538 | console.log("Transaction rolled back due to error:", error); 539 | throw error; 540 | } 541 | }; 542 | 543 | const storeProofOfAuthorization = async function (importantDataToStore) { 544 | // We're not going to implement this function in this example, but you 545 | // should store this data for at least two years. 546 | console.log("Storing proof of authorization data:"); 547 | console.log(JSON.stringify(importantDataToStore)); 548 | }; 549 | 550 | /********************** 551 | * App Data -- Fetch (and store) the last event we synced 552 | **********************/ 553 | const getLastSyncNum = async function () { 554 | try { 555 | const maybeRow = await db.get( 556 | `SELECT key, value from appdata WHERE key = 'last_sync'` 557 | ); 558 | if (maybeRow == null) { 559 | return null; 560 | } 561 | return Number(maybeRow.value); 562 | } catch (error) { 563 | console.error(`Error getting last sync number ${error}`); 564 | throw error; 565 | } 566 | }; 567 | 568 | const setLastSyncNum = async function (syncNum) { 569 | try { 570 | await db.run( 571 | `INSERT INTO appdata (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`, 572 | ["last_sync", syncNum.toString()] 573 | ); 574 | } catch (error) { 575 | console.error(`Error setting last sync number: ${error}`); 576 | throw error; 577 | } 578 | }; 579 | 580 | /******************************** 581 | * I'm calling these "admin" functions, in that we don't check the userID 582 | * Meaning they shouldn't be called in response to a user action 583 | *******************************/ 584 | 585 | const adminGetBillDetails = async function (billId) { 586 | try { 587 | const billDetails = await db.get( 588 | `SELECT * FROM bills where id = ?`, 589 | billId 590 | ); 591 | return billDetails; 592 | } catch (error) { 593 | console.error(`Error getting last sync number ${error}`); 594 | throw error; 595 | } 596 | }; 597 | 598 | const adminGetPaymentsForBill = async function (billId) { 599 | try { 600 | const payments = await db.all( 601 | `SELECT * FROM payments WHERE bill_id = ?`, 602 | billId 603 | ); 604 | return payments; 605 | } catch (error) { 606 | console.error(`Error getting payments for bill ${error}`); 607 | throw error; 608 | } 609 | }; 610 | 611 | const adminUpdateBillStatus = async function ( 612 | billId, 613 | newBillStatus, 614 | settledTotal, 615 | pendingTotal 616 | ) { 617 | try { 618 | const updateResult = await db.run( 619 | `UPDATE bills SET status=?, paid_total_cents=?, pending_total_cents=? WHERE id = ?`, 620 | newBillStatus, 621 | settledTotal, 622 | pendingTotal, 623 | billId 624 | ); 625 | return updateResult; 626 | } catch (error) { 627 | console.error(`Error updating bill status ${error}`); 628 | throw error; 629 | } 630 | }; 631 | 632 | module.exports = { 633 | debugExposeDb, 634 | getAccessTokenForUserAndAccount, 635 | getAccessTokenForUserAndItem, 636 | getItemsAndAccountsForUser, 637 | getItemInfoForAccountAndUser, 638 | addUser, 639 | getUserList, 640 | getUserRecord, 641 | getBankNamesForUser, 642 | addItem, 643 | addBankNameForItem, 644 | addAccount, 645 | createNewBill, 646 | getBillsForUser, 647 | getBillDetailsForUser, 648 | createPaymentForUser, 649 | addPaymentAuthorization, 650 | updatePaymentWithTransferIntent, 651 | updatePaymentWithAccountId, 652 | updatePaymentWithTransferInfo, 653 | storeProofOfAuthorization, 654 | getPaymentByPlaidId, 655 | getPaymentsForUserBill, 656 | updatePaymentStatus, 657 | getLastSyncNum, 658 | setLastSyncNum, 659 | adminGetBillDetails, 660 | adminGetPaymentsForBill, 661 | adminUpdateBillStatus, 662 | }; 663 | -------------------------------------------------------------------------------- /server/middleware/authenticate.js: -------------------------------------------------------------------------------- 1 | const { getLoggedInUserId } = require("../utils"); 2 | 3 | /** 4 | * Middleware that checks if the user is logged in. 5 | * If so, we'll attach the userId to the request. 6 | * If not, we'll send a 401 response. 7 | */ 8 | const authenticate = function (req, res, next) { 9 | try { 10 | const userId = getLoggedInUserId(req); 11 | if (!userId) { 12 | // User is not logged in, send an appropriate response 13 | return res.status(401).json({ message: "Unauthorized: Please log in." }); 14 | } 15 | // User is logged in, attach userId to the request for further use 16 | req.userId = userId; 17 | next(); // Proceed to the next middleware or route handler 18 | } catch (error) { 19 | return res 20 | .status(500) 21 | .json({ message: "An error occurred during authentication." }); 22 | } 23 | }; 24 | 25 | module.exports = authenticate; 26 | -------------------------------------------------------------------------------- /server/plaid.js: -------------------------------------------------------------------------------- 1 | const PLAID_ENV = (process.env.PLAID_ENV || "sandbox").toLowerCase(); 2 | const { Configuration, PlaidEnvironments, PlaidApi } = require("plaid"); 3 | 4 | /** 5 | * Set up the Plaid Client Library. With our configuration object, we can 6 | * make sure that the CLIENT_ID and SECRET are always included in our requests 7 | * to the Plaid API. 8 | */ 9 | const plaidConfig = new Configuration({ 10 | basePath: PlaidEnvironments[PLAID_ENV], 11 | baseOptions: { 12 | headers: { 13 | "PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID, 14 | "PLAID-SECRET": process.env.PLAID_SECRET, 15 | "Plaid-Version": "2020-09-14", 16 | }, 17 | }, 18 | }); 19 | 20 | const plaidClient = new PlaidApi(plaidConfig); 21 | 22 | module.exports = { plaidClient }; 23 | -------------------------------------------------------------------------------- /server/recalculateBills.js: -------------------------------------------------------------------------------- 1 | const db = require("./db"); 2 | const { PAYMENT_STATUS, BILL_STATUS } = require("./types"); 3 | 4 | /** 5 | * Recalculate our bill by looking at all of the payments 6 | * associated with this bill, adding up what's been paid, what's still 7 | * pending, and then updating its status accordingly 8 | */ 9 | async function recalculateBill(billId) { 10 | // 1. Get all payments related to our bill 11 | const billDetails = await db.adminGetBillDetails(billId); 12 | const payments = await db.adminGetPaymentsForBill(billId); 13 | 14 | // 2. For any payment that's marked "settled", let's add it to our settled total 15 | const settledTotal = payments 16 | .filter((payment) => payment.status == PAYMENT_STATUS.SETTLED) 17 | .reduce((prev, payment) => prev + payment.amount_cents, 0); 18 | 19 | // 3. For any payment that's marked "pending" or "posted", let's add it to our pending amount 20 | 21 | const pendingTotal = payments 22 | .filter( 23 | (payment) => 24 | payment.status == PAYMENT_STATUS.PENDING || 25 | payment.status == PAYMENT_STATUS.POSTED 26 | ) 27 | .reduce((prev, payment) => prev + payment.amount_cents, 0); 28 | 29 | console.log( 30 | `For bill ${billId}, ${settledTotal} has been paid, ${pendingTotal} is pending` 31 | ); 32 | 33 | // How you want to customize bill status to the user is up to you. This is just 34 | // one example. 35 | let newBillStatus = BILL_STATUS.UNPAID; 36 | if (settledTotal >= billDetails.original_amount_cents) { 37 | newBillStatus = BILL_STATUS.PAID; 38 | } else if (settledTotal + pendingTotal >= billDetails.original_amount_cents) { 39 | newBillStatus = BILL_STATUS.PAID_PENDING; 40 | } else if (settledTotal > 0 || pendingTotal > 0) { 41 | newBillStatus = BILL_STATUS.PARTIALLY_PAID; 42 | } 43 | db.adminUpdateBillStatus(billId, newBillStatus, settledTotal, pendingTotal); 44 | } 45 | 46 | module.exports = { recalculateBill }; 47 | -------------------------------------------------------------------------------- /server/routes/banks.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const db = require("../db"); 3 | const { plaidClient } = require("../plaid"); 4 | const authenticate = require("../middleware/authenticate"); 5 | 6 | const router = express.Router(); 7 | router.use(authenticate); 8 | 9 | /** 10 | * List all the banks (by name) that the user has connected to so far. 11 | */ 12 | router.get("/list", async (req, res, next) => { 13 | try { 14 | const userId = req.userId; 15 | const result = await db.getBankNamesForUser(userId); 16 | res.json(result); 17 | } catch (error) { 18 | next(error); 19 | } 20 | }); 21 | 22 | /** 23 | * List all the accounts that the user has connected to so far. 24 | */ 25 | router.get("/accounts/list", async (req, res, next) => { 26 | try { 27 | const userId = req.userId; 28 | const result = await db.getItemsAndAccountsForUser(userId); 29 | res.json(result); 30 | } catch (error) { 31 | next(error); 32 | } 33 | }); 34 | 35 | 36 | /** 37 | * Deactivate a bank account for the user. 38 | * We don't actually call this endpoint in our app, but it's good to have 39 | * around. 40 | */ 41 | router.post("/deactivate", async (req, res, next) => { 42 | try { 43 | const itemId = req.body.itemId; 44 | const userId = req.userId; 45 | console.log("Deactivating item", itemId, "for user", userId); 46 | const accessToken = await db.getAccessTokenForUserAndItem(userId, itemId); 47 | console.log("Access token:", accessToken); 48 | await plaidClient.itemRemove({ 49 | access_token: accessToken, 50 | }); 51 | await db.deactivateItem(itemId); 52 | res.json({ removed: itemId }); 53 | } catch (error) { 54 | next(error); 55 | } 56 | }); 57 | 58 | module.exports = router; 59 | -------------------------------------------------------------------------------- /server/routes/bills.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { getUserObject } = require("../utils"); 3 | const db = require("../db"); 4 | const authenticate = require("../middleware/authenticate"); 5 | 6 | const router = express.Router(); 7 | router.use(authenticate); 8 | 9 | /** 10 | * Generate a new utility bill for our user. 11 | */ 12 | router.post("/create", async (req, res, next) => { 13 | try { 14 | const userId = req.userId; 15 | const result = await db.createNewBill(userId); 16 | console.log(`Bill creation result is ${JSON.stringify(result)}`); 17 | res.json(result); 18 | } catch (error) { 19 | next(error); 20 | } 21 | }); 22 | 23 | /** 24 | * List all the bills for the current signed-in user. 25 | */ 26 | router.get("/list", async (req, res, next) => { 27 | try { 28 | const userId = req.userId; 29 | const result = await db.getBillsForUser(userId); 30 | res.json(result); 31 | } catch (error) { 32 | next(error); 33 | } 34 | }); 35 | 36 | /** 37 | * Get the details of a specific bill for the current signed-in user. 38 | */ 39 | router.post("/get", async (req, res, next) => { 40 | try { 41 | const userId = req.userId; 42 | const { billId } = req.body; 43 | const result = await db.getBillDetailsForUser(userId, billId); 44 | res.json(result); 45 | } catch (error) { 46 | next(error); 47 | } 48 | }); 49 | 50 | module.exports = router; 51 | -------------------------------------------------------------------------------- /server/routes/debug.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const db = require("../db"); 3 | const { plaidClient } = require("../plaid"); 4 | const authenticate = require("../middleware/authenticate"); 5 | const { syncPaymentData } = require("../syncPaymentData"); 6 | 7 | const router = express.Router(); 8 | router.use(authenticate); 9 | /** 10 | * Sometimes you wanna run some custom server code. This seemed like the 11 | * easiest way to do it. Don't do this in a real application. 12 | */ 13 | router.post("/run", async (req, res, next) => { 14 | try { 15 | const userId = req.userId; 16 | res.json({ status: "done" }); 17 | } catch (error) { 18 | next(error); 19 | } 20 | }); 21 | 22 | /** 23 | * Sync all the payment data from Plaid to our database. 24 | * Normally, you might run this from a cron job, or in response to a webhook. 25 | * For the sake of this demo, we'll just expose it as an endpoint. 26 | * 27 | */ 28 | router.post("/sync_events", async (req, res, next) => { 29 | try { 30 | await syncPaymentData(); 31 | res.json({ status: "done" }); 32 | } catch (error) { 33 | next(error); 34 | } 35 | }); 36 | 37 | /** 38 | * Fire a webhook to simulate what might happen if a transfer's status changed. 39 | * Normally, these would happen automatically in response to a transfer's 40 | * status changing. In Sandbox, you need to call this explicity -- even if 41 | * you changed the transfer's status in the Plaid Dashboard. 42 | */ 43 | router.post("/fire_webhook", async (req, res, next) => { 44 | try { 45 | const webhookUrl = process.env.SANDBOX_WEBHOOK_URL; 46 | await plaidClient.sandboxTransferFireWebhook({ 47 | webhook: webhookUrl, 48 | }); 49 | res.json({ status: "done" }); 50 | } catch (error) { 51 | next(error); 52 | } 53 | }); 54 | 55 | module.exports = router; 56 | -------------------------------------------------------------------------------- /server/routes/payments.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { plaidClient } = require("../plaid"); 3 | const db = require("../db"); 4 | const authenticate = require("../middleware/authenticate"); 5 | 6 | const router = express.Router(); 7 | router.use(authenticate); 8 | 9 | const WEBHOOK_URL = 10 | process.env.WEBHOOK_URL || "https://www.example.com/server/receive_webhook"; 11 | 12 | /** 13 | * Initiates a transfer payment using Plaid Transfer. 14 | * This function creates a transfer intent, records the payment attempt in the 15 | * database,and then passes that transfer intent ID along to /link/token/create 16 | * to get a link token. 17 | */ 18 | router.post("/initiate", async (req, res, next) => { 19 | try { 20 | const userId = req.userId; 21 | const { billId, accountId, amount } = req.body; 22 | // Grab our user's legal name 23 | const userObject = await db.getUserRecord(userId); 24 | const legalName = `${userObject.first_name} ${userObject.last_name}`.trim() || "Test User"; 25 | const amountAsString = Number.parseFloat(amount).toFixed(2); 26 | const amountAsCents = Math.round(amount * 100); 27 | // Let's just make sure we normalize the accountID. 28 | const accountIdOrNull = 29 | accountId != null && accountId !== "new" && accountId !== "" 30 | ? accountId 31 | : null; 32 | 33 | // Call transferIntentCreate to invoke the transfer UI 34 | const transferIntentId = await getTransferIntentId( 35 | legalName, 36 | amountAsString, 37 | billId, 38 | accountIdOrNull 39 | ); 40 | 41 | // Save this attempted payment in the database 42 | await db.createPaymentForUser( 43 | userId, 44 | billId, 45 | transferIntentId, 46 | accountIdOrNull, 47 | amountAsCents 48 | ); 49 | 50 | // Now create a link token 51 | const linkToken = await createLinkTokenForTransferUI( 52 | userId, 53 | legalName, 54 | transferIntentId, 55 | accountIdOrNull 56 | ); 57 | 58 | res.json({ linkToken, transferIntentId }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | }); 63 | 64 | /** 65 | * Performs post-transfer actions. At this point, the transfer has already been 66 | * completed, but we need to make sure our app knows about it. 67 | * 68 | * This function retrieves the transfer intent details, updates the payment 69 | * record in the database, and fetches account details if a new 70 | * account was connected during the transfer process. 71 | */ 72 | router.post("/transfer_ui_complete", async (req, res, next) => { 73 | const { transferIntentId } = req.body; 74 | const userId = req.userId; 75 | try { 76 | const response = await plaidClient.transferIntentGet({ 77 | transfer_intent_id: transferIntentId, 78 | }); 79 | const intentData = response.data.transfer_intent; 80 | console.dir(intentData, { depth: null }); 81 | 82 | await db.updatePaymentWithTransferIntent( 83 | userId, 84 | transferIntentId, 85 | intentData.transfer_id, 86 | intentData.account_id, 87 | intentData.authorization_decision, 88 | intentData.authorization_decision_rationale 89 | ? intentData.authorization_decision_rationale.description 90 | : null, 91 | intentData.status 92 | ); 93 | 94 | if (intentData.account_id == null) { 95 | console.log( 96 | "No account ID from the transfer intent, which means the user connected to a new one. Let's fetch that detail from the transfer" 97 | ); 98 | const transferResponse = await plaidClient.transferGet({ 99 | transfer_id: intentData.transfer_id, 100 | }); 101 | console.dir(transferResponse.data, { depth: null }); 102 | await db.updatePaymentWithAccountId( 103 | userId, 104 | intentData.transfer_id, 105 | transferResponse.data.transfer.account_id 106 | ); 107 | } 108 | 109 | res.json({ status: "success" }); 110 | } catch (error) { 111 | next(error); 112 | } 113 | }); 114 | 115 | /** 116 | * Lists payments for a specific bill. 117 | */ 118 | router.post("/list", async (req, res, next) => { 119 | try { 120 | const userId = req.userId; 121 | const billId = req.body.billId; 122 | const payments = await db.getPaymentsForUserBill(userId, billId); 123 | res.json(payments); 124 | } catch (error) { 125 | next(error); 126 | } 127 | }); 128 | 129 | /** 130 | * Creates a Transfer Intent using the Plaid API, and returns the transfer 131 | * intent ID. 132 | */ 133 | async function getTransferIntentId( 134 | legalName, 135 | amountAsString, 136 | billId, 137 | accountIdOrNull 138 | ) { 139 | const intentCreateObject = { 140 | mode: "PAYMENT", // Used for transfer going from the end-user to you 141 | user: { 142 | legal_name: legalName, 143 | }, 144 | amount: amountAsString, 145 | description: "BillPay", 146 | ach_class: "ppd", // Refer to the documentation, or talk to your Plaid representative to see which class is right for you. 147 | iso_currency_code: "USD", 148 | network: "same-day-ach", // This is the default value, but I like to make it explicit 149 | metadata: { 150 | bill_id: billId, 151 | }, 152 | }; 153 | if (accountIdOrNull != null) { 154 | intentCreateObject.account_id = accountIdOrNull; 155 | } 156 | 157 | console.log(intentCreateObject); 158 | const response = await plaidClient.transferIntentCreate(intentCreateObject); 159 | console.log(response.data); 160 | // We'll return the transfer intent ID to the client so they can start 161 | // transfer UI 162 | return response.data.transfer_intent.id; 163 | } 164 | 165 | /** 166 | * Creates a link token to be used for initiating transfer. By passing along 167 | * the transfer intent ID that we created in an earlier step, Link will know all 168 | * about the transfer that we want to make. 169 | */ 170 | async function createLinkTokenForTransferUI( 171 | userId, 172 | legalName, 173 | transferIntentId, 174 | accountIdOrNull 175 | ) { 176 | const linkTokenCreateObject = { 177 | user: { 178 | client_user_id: userId, 179 | legal_name: legalName, 180 | }, 181 | products: ["transfer"], 182 | transfer: { 183 | intent_id: transferIntentId, 184 | }, 185 | client_name: "Pay My Utility Bill", 186 | language: "en", 187 | country_codes: ["US"], 188 | webhook: WEBHOOK_URL, 189 | }; 190 | if (accountIdOrNull != null) { 191 | const accessToken = await db.getAccessTokenForUserAndAccount( 192 | userId, 193 | accountIdOrNull 194 | ); 195 | console.log(`Access token for account ${accountIdOrNull}: ${accessToken}`); 196 | 197 | linkTokenCreateObject.access_token = accessToken; 198 | } 199 | console.log(linkTokenCreateObject); 200 | 201 | const response = await plaidClient.linkTokenCreate(linkTokenCreateObject); 202 | console.log(response.data); 203 | 204 | return response.data.link_token; 205 | } 206 | 207 | module.exports = router; 208 | -------------------------------------------------------------------------------- /server/routes/payments_no_transferUI.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { plaidClient } = require("../plaid"); 3 | const db = require("../db"); 4 | const authenticate = require("../middleware/authenticate"); 5 | 6 | const router = express.Router(); 7 | router.use(authenticate); 8 | 9 | /************** 10 | * Want to initialize a payment without Transfer UI? These endpoints here 11 | * will help you do that. 12 | * 13 | * The first part of the process is storing the proof of authorization. This is 14 | * necessary for Nacha compliance and Plaid may ask you for this data to settle 15 | * disputes. 16 | * 17 | * For more information, see: https://www.nacha.org/system/files/2022-11/WEB_Proof_of_Authorization_Industry_Practices.pdf 18 | * 19 | ***************/ 20 | 21 | 22 | /** 23 | * Store the proof of authorization for a payment. This is necessary for Nacha 24 | * compliance. This is obviously not a fully functional endpoint, but it should 25 | * give you an idea of what you need to do if you choose not to use Link's 26 | * Transfer UI 27 | */ 28 | router.post( 29 | "/store_proof_of_authorization_necessary_for_nacha_compliance", 30 | async (req, res, next) => { 31 | const userId = req.userId; 32 | const { billId, accountId, amount } = req.body; 33 | const { created_time: account_verification_time } = 34 | await db.getItemInfoForAccountAndUser(accountId, userId); 35 | 36 | const importantDataToStore = { 37 | accountAuthorizationMethod: `Plaid Auth`, 38 | accountAuthorizationTime: account_verification_time, 39 | ipAddress: req.ip, 40 | howWeVerifiedUserID: "(Sign-in details here)", 41 | howWeVerifiedUser: "(Details here)", 42 | timestamp: new Date(), 43 | userID: req.userId, 44 | userClickedConsent: true, 45 | oneTimeOrRecurring: "One time", 46 | metadata: { billId, accountId, amount }, 47 | }; 48 | 49 | await db.storeProofOfAuthorization(importantDataToStore); 50 | res.json({ 51 | status: "success", 52 | message: "Ready to submit pamyment", 53 | }); 54 | } 55 | ); 56 | 57 | /** 58 | * 59 | * We perform two steps here. First, we authorize the transfer, which performs 60 | * important steps like checking the user's balance to see if they have enough 61 | * to cover the payment. 62 | * 63 | * Second, if the authorization is successful, we then create the transfer. 64 | */ 65 | 66 | router.post("/authorize_and_create", async (req, res, next) => { 67 | try { 68 | const userId = req.userId; 69 | const { billId, accountId, amount } = req.body; 70 | const amountAsCents = Math.round(amount * 100); 71 | const accessToken = await db.getAccessTokenForUserAndAccount( 72 | userId, 73 | accountId 74 | ); 75 | const paymentAsString = Number.parseFloat(amount).toFixed(2); 76 | 77 | // Let's add this to the database first 78 | const paymentId = await db.createPaymentForUser( 79 | userId, 80 | billId, 81 | null, 82 | accountId, 83 | amountAsCents 84 | ); 85 | const { authStatus, decisionMessage, authId } = await authorizeTransfer( 86 | accessToken, 87 | accountId, 88 | userId, 89 | paymentAsString, 90 | paymentId 91 | ); 92 | if (authStatus === "rejected") { 93 | res.json({ 94 | status: "rejected", 95 | message: decisionMessage, 96 | }); 97 | return; 98 | } else if (authStatus === "unsure") { 99 | // How you handle this is up to you. You may want to proceed with the 100 | // transfer if you decide this user is low-risk 101 | res.json({ status: "unsure", message: decisionMessage }); 102 | return; 103 | } 104 | 105 | // Let's create a transfer 106 | const { 107 | id: transferId, 108 | status: transferStatus, 109 | failureReason, 110 | } = await createTransferAfterAuthorization( 111 | accessToken, 112 | accountId, 113 | billId, 114 | paymentId, 115 | paymentAsString, 116 | authId 117 | ); 118 | 119 | if (transferStatus === "failed") { 120 | res.json({ status: "failed", message: failureReason }); 121 | return; 122 | } 123 | 124 | res.json({ status: "success", message: "Your payment has been submitted" }); 125 | } catch (error) { 126 | next(error); 127 | } 128 | }); 129 | 130 | /** 131 | * Make a call to Plaid to authorize the transfer -- this checks that the account 132 | * is valid and that the user has sufficient funds to cover the payment. 133 | * 134 | * Note that when receiving a response from the /transfer/authorization/create 135 | * endpoint, Plaid will default to "approved" in cases where it can't properly 136 | * fetch account balance data from the user's bank. You should always check the 137 | * decision_rationale field to see if there were any issues and then decide 138 | * for yourself how to proceed. 139 | * 140 | */ 141 | 142 | async function authorizeTransfer( 143 | accessToken, 144 | accountId, 145 | userId, 146 | paymentAsString, 147 | paymentId 148 | ) { 149 | const userInfo = await db.getUserRecord(userId); 150 | const legalName = `${userInfo.first_name} ${userInfo.last_name}`.trim() || "Test User"; 151 | 152 | const response = await plaidClient.transferAuthorizationCreate({ 153 | access_token: accessToken, 154 | account_id: accountId, 155 | type: "debit", 156 | amount: paymentAsString, 157 | network: "same-day-ach", 158 | idempotency_key: paymentId, 159 | ach_class: "ppd", // Refer to the documentation, or talk to your Plaid representative to see which class is right for you. 160 | user_present: true, 161 | user: { 162 | legal_name: legalName, 163 | }, 164 | }); 165 | console.dir(response.data, { depth: null }); 166 | const authObject = response.data.authorization; 167 | let authStatus = ""; 168 | 169 | if (authObject.decision === "declined") { 170 | authStatus = "rejected"; 171 | } else if ( 172 | authObject.decision === "approved" && 173 | authObject.decision_rationale == null 174 | ) { 175 | authStatus = "authorized"; 176 | } else { 177 | authStatus = "unsure"; 178 | } 179 | 180 | let decisionMessage = authObject.decision_rationale?.description ?? ""; 181 | 182 | // Store this in the database 183 | await db.addPaymentAuthorization( 184 | paymentId, 185 | authObject.id, 186 | authStatus, 187 | decisionMessage 188 | ); 189 | 190 | return { authStatus, decisionMessage, authId: authObject.id }; 191 | } 192 | 193 | /** 194 | * Once the authorization step is complete, we can go ahead and create the 195 | * transfer in Plaid's system. 196 | */ 197 | 198 | async function createTransferAfterAuthorization( 199 | accessToken, 200 | accountId, 201 | billId, 202 | paymentId, 203 | paymentAsString, 204 | authId 205 | ) { 206 | const transferResponse = await plaidClient.transferCreate({ 207 | access_token: accessToken, 208 | account_id: accountId, 209 | description: "Payment", 210 | amount: paymentAsString, 211 | metadata: { 212 | bill_id: billId, 213 | }, 214 | authorization_id: authId, 215 | }); 216 | 217 | console.log(transferResponse.data); 218 | const transferObject = transferResponse.data.transfer; 219 | 220 | // Let's update the transfer status 221 | await db.updatePaymentWithTransferInfo( 222 | paymentId, 223 | transferObject.id, 224 | transferObject.status, 225 | transferObject.failure_reason ?? "" 226 | ); 227 | return { 228 | id: transferObject.id, 229 | status: transferObject.status, 230 | failureReason: transferObject.failure_reason ?? "", 231 | }; 232 | } 233 | module.exports = router; 234 | -------------------------------------------------------------------------------- /server/routes/tokens.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const escape = require("escape-html"); 3 | const db = require("../db"); 4 | const { plaidClient } = require("../plaid"); 5 | const authenticate = require("../middleware/authenticate"); 6 | 7 | const router = express.Router(); 8 | router.use(authenticate); 9 | 10 | /** 11 | * Creates a link token to be used by the client. This is currently only used 12 | * if you are not using Link's Transfer UI. Otherwise, you'll get a Link token 13 | * by calling the payments/initiate endpoint. 14 | */ 15 | router.post("/create_link_token", async (req, res, next) => { 16 | try { 17 | const userId = req.userId; 18 | const userObject = { id: userId }; 19 | const link_token = await generateLinkToken(userObject); 20 | res.json({ link_token }); 21 | } catch (error) { 22 | console.log(`Running into an error!`); 23 | next(error); 24 | } 25 | }); 26 | 27 | /** 28 | * Exchanges a public token for an access token. Then, fetches a bunch of 29 | * information about that item and stores it in our database 30 | */ 31 | router.post("/exchange_public_token", async (req, res, next) => { 32 | try { 33 | const userId = req.userId; 34 | const publicToken = escape(req.body.publicToken); 35 | const returnAccountId = escape(req.body.returnAccountId); 36 | 37 | const tokenResponse = await plaidClient.itemPublicTokenExchange({ 38 | public_token: publicToken, 39 | }); 40 | const tokenData = tokenResponse.data; 41 | await db.addItem(tokenData.item_id, userId, tokenData.access_token); 42 | await populateBankName(tokenData.item_id, tokenData.access_token); 43 | await populateAccountNames(tokenData.access_token); 44 | 45 | let accountId = ""; 46 | if (returnAccountId) { 47 | // Let's grab an account from the item that was just added 48 | const acctsResponse = await plaidClient.accountsGet({ 49 | access_token: tokenData.access_token, 50 | }); 51 | const acctsData = acctsResponse.data; 52 | accountId = acctsData.accounts[0].account_id; 53 | } 54 | 55 | res.json({ status: "success", accountId: accountId }); 56 | } catch (error) { 57 | console.log(`Running into an error!`); 58 | next(error); 59 | } 60 | }); 61 | 62 | /** 63 | * Grabs the name of the bank that the user has connected to. This actually 64 | * requires two different calls -- one to get the institution ID associated with 65 | * the Item, and then another to fetch the name of the institution. 66 | */ 67 | const populateBankName = async (itemId, accessToken) => { 68 | try { 69 | const itemResponse = await plaidClient.itemGet({ 70 | access_token: accessToken, 71 | }); 72 | const institutionId = itemResponse.data.item.institution_id; 73 | if (institutionId == null) { 74 | return; 75 | } 76 | const institutionResponse = await plaidClient.institutionsGetById({ 77 | institution_id: institutionId, 78 | country_codes: ["US"], 79 | }); 80 | const institutionName = institutionResponse.data.institution.name; 81 | await db.addBankNameForItem(itemId, institutionName); 82 | } catch (error) { 83 | console.log(`Ran into an error! ${error}`); 84 | } 85 | }; 86 | 87 | /** 88 | * Let's grab the names of the accounts that the user has connected to and 89 | * store them in our database. 90 | */ 91 | const populateAccountNames = async (accessToken) => { 92 | try { 93 | const acctsResponse = await plaidClient.accountsGet({ 94 | access_token: accessToken, 95 | }); 96 | const acctsData = acctsResponse.data; 97 | const itemId = acctsData.item.item_id; 98 | await Promise.all( 99 | acctsData.accounts.map(async (acct) => { 100 | await db.addAccount( 101 | acct.account_id, 102 | itemId, 103 | acct.name, 104 | acct.balances.available ?? acct.balances.current 105 | ); 106 | }) 107 | ); 108 | } catch (error) { 109 | console.log(`Ran into an error! ${error}`); 110 | } 111 | }; 112 | 113 | /** 114 | * Performs the work of actually generating a link token from the Plaid API. 115 | */ 116 | const generateLinkToken = async (userObj) => { 117 | const tokenResponse = await plaidClient.linkTokenCreate({ 118 | user: { client_user_id: userObj.id }, 119 | products: ["transfer"], 120 | client_name: "Bill Transfer App", 121 | language: "en", 122 | country_codes: ["US"], 123 | }); 124 | // Send this back to your client 125 | return tokenResponse.data.link_token; 126 | }; 127 | 128 | module.exports = router; 129 | -------------------------------------------------------------------------------- /server/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const escape = require("escape-html"); 3 | const { v4: uuidv4 } = require("uuid"); 4 | const db = require("../db"); 5 | const authenticate = require("../middleware/authenticate"); 6 | 7 | const router = express.Router(); 8 | 9 | /****************************************** 10 | * Methods and endpoints for signing in, signing out, and creating new users. 11 | * For the purpose of this sample, we're simply setting / fetching a cookie that 12 | * contains the userID as our way of getting the ID of our signed-in user. 13 | ******************************************/ 14 | 15 | 16 | /** 17 | * Create a new user! Then, we can set a cookie to remember that user. 18 | */ 19 | router.post("/create", async (req, res, next) => { 20 | try { 21 | const username = escape(req.body.username); 22 | const firstName = escape(req.body.firstName); 23 | const lastName = escape(req.body.lastName); 24 | const userId = uuidv4(); 25 | const result = await db.addUser(userId, username, firstName, lastName); 26 | console.log(`User creation result is ${JSON.stringify(result)}`); 27 | if (result["lastID"] != null) { 28 | res.cookie("signedInUser", userId, { 29 | maxAge: 1000 * 60 * 60 * 24 * 30, 30 | httpOnly: true, 31 | }); 32 | } 33 | res.json(result); 34 | } catch (error) { 35 | next(error); 36 | } 37 | }); 38 | 39 | 40 | /** 41 | * List all the users in our database. 42 | */ 43 | router.get("/list", async (req, res, next) => { 44 | try { 45 | const result = await db.getUserList(); 46 | res.json(result); 47 | } catch (error) { 48 | next(error); 49 | } 50 | }); 51 | 52 | /** 53 | * Sign in as an existing user. 54 | */ 55 | router.post("/sign_in", async (req, res, next) => { 56 | try { 57 | const userId = escape(req.body.userId); 58 | res.cookie("signedInUser", userId, { 59 | maxAge: 1000 * 60 * 60 * 24 * 30, 60 | httpOnly: true, 61 | }); 62 | res.json({ signedIn: true }); 63 | } catch (error) { 64 | next(error); 65 | } 66 | }); 67 | 68 | /** 69 | * Sign out the current user from our app. This is as simple as clearing the 70 | * cookie. 71 | */ 72 | router.post("/sign_out", async (req, res, next) => { 73 | try { 74 | res.clearCookie("signedInUser"); 75 | res.json({ signedOut: true }); 76 | } catch (error) { 77 | next(error); 78 | } 79 | }); 80 | 81 | /** 82 | * Get some information about our currently logged-in user (if there is one). 83 | */ 84 | router.get("/get_my_info", authenticate, async (req, res, next) => { 85 | try { 86 | const userId = req.userId; 87 | console.log(`Your userID is ${userId}`); 88 | let result; 89 | if (userId != null) { 90 | const userObject = await db.getUserRecord(userId); 91 | if (userObject == null) { 92 | // This probably means your cookies are messed up. 93 | res.clearCookie("signedInUser"); 94 | res.json({ userInfo: null }); 95 | return; 96 | } else { 97 | result = { 98 | id: userObject.id, 99 | username: userObject.username, 100 | firstName: userObject.first_name, 101 | lastName: userObject.last_name, 102 | }; 103 | } 104 | } else { 105 | result = null; 106 | } 107 | res.json({ userInfo: result }); 108 | } catch (error) { 109 | next(error); 110 | } 111 | }); 112 | 113 | module.exports = router; 114 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const express = require("express"); 3 | const bodyParser = require("body-parser"); 4 | const cookieParser = require("cookie-parser"); 5 | 6 | const APP_PORT = process.env.APP_PORT || 8000; 7 | 8 | /** 9 | * Initialization! 10 | */ 11 | 12 | // Set up the server 13 | const app = express(); 14 | app.use(cookieParser()); 15 | app.use(bodyParser.urlencoded({ extended: false })); 16 | app.use(bodyParser.json()); 17 | app.use(express.static("./public")); 18 | 19 | const server = app.listen(APP_PORT, function () { 20 | console.log(`Server is up and running at http://localhost:${APP_PORT}/`); 21 | }); 22 | 23 | // Add in all the routes 24 | const usersRouter = require("./routes/users"); 25 | const linkTokenRouter = require("./routes/tokens"); 26 | const banksRouter = require("./routes/banks"); 27 | const billsRouter = require("./routes/bills"); 28 | const paymentsRouter = require("./routes/payments"); 29 | const paymentsNoTUIRouter = require("./routes/payments_no_transferUI"); 30 | const debugRouter = require("./routes/debug"); 31 | const { getWebhookServer } = require("./webhookServer"); 32 | 33 | app.use("/server/users", usersRouter); 34 | app.use("/server/tokens", linkTokenRouter); 35 | app.use("/server/banks", banksRouter); 36 | app.use("/server/bills", billsRouter); 37 | app.use("/server/payments", paymentsRouter); 38 | app.use("/server/payments/no_transfer_ui", paymentsNoTUIRouter); 39 | app.use("/server/debug", debugRouter); 40 | 41 | /** 42 | * Add in some basic error handling so our server doesn't crash if we run into 43 | * an error. 44 | */ 45 | const errorHandler = function (err, req, res, next) { 46 | console.error(`Your error:`); 47 | console.error(err.response?.data); 48 | if (err.response?.data != null) { 49 | res.status(500).send(err.response.data); 50 | } else { 51 | res.status(500).send({ 52 | error_code: "OTHER_ERROR", 53 | error_message: "I got some other message on the server.", 54 | }); 55 | console.log(`Error object: ${JSON.stringify(err)}`); 56 | } 57 | }; 58 | app.use(errorHandler); 59 | 60 | // Let's start the webhook server while we're at it 61 | const webhookServer = getWebhookServer(); 62 | -------------------------------------------------------------------------------- /server/syncPaymentData.js: -------------------------------------------------------------------------------- 1 | const { plaidClient } = require("./plaid"); 2 | const db = require("./db"); 3 | const { PAYMENT_STATUS } = require("./types"); 4 | 5 | // Let's define all the valid transitions between payment statuses. It's a 6 | // simple way to ensure that we're not accidentally updating a payment to an 7 | // invalid state. (This won't happen in Plaid's system, but it might happen 8 | // in development if you end up re-processing the same batch of events. 9 | 10 | const EXPECTED_NEXT_STATES = { 11 | [PAYMENT_STATUS.NEW]: [PAYMENT_STATUS.PENDING], 12 | [PAYMENT_STATUS.PENDING]: [ 13 | PAYMENT_STATUS.PENDING, 14 | PAYMENT_STATUS.FAILED, 15 | PAYMENT_STATUS.POSTED, 16 | PAYMENT_STATUS.CANCELLED, 17 | ], 18 | [PAYMENT_STATUS.POSTED]: [PAYMENT_STATUS.SETTLED, PAYMENT_STATUS.RETURNED], 19 | [PAYMENT_STATUS.SETTLED]: [PAYMENT_STATUS.RETURNED], 20 | }; 21 | 22 | /** 23 | * Sync all the payment data from Plaid to our database. 24 | * We'll start from the last sync number we have stored in our database, 25 | * fetch events in batches of 20, and process them one by one. 26 | * We'll keep going until the has_more field is false. 27 | */ 28 | async function syncPaymentData() { 29 | let lastSyncNum = 30 | (await db.getLastSyncNum()) ?? Number(process.env.START_SYNC_NUM); 31 | console.log(`Last sync number is ${lastSyncNum}`); 32 | 33 | let fetchMore = true; 34 | while (fetchMore) { 35 | const nextBatch = await plaidClient.transferEventSync({ 36 | after_id: lastSyncNum, 37 | count: 20, 38 | }); 39 | const sortedEvents = nextBatch.data.transfer_events.sort( 40 | (a, b) => a.event_id - b.event_id 41 | ); 42 | 43 | for (const event of sortedEvents) { 44 | await processPaymentEvent(event); 45 | lastSyncNum = event.event_id; 46 | } 47 | // The has_more field was just added in March of 2024! 48 | fetchMore = nextBatch.data.has_more; 49 | } 50 | await db.setLastSyncNum(lastSyncNum); 51 | } 52 | 53 | /** 54 | * Process a /sync event from Plaid. These events are sent to us when a transfer 55 | * changes status. Typically, they go from `pending` to `posted` to `settled`, 56 | * but there are other things that can happen along the way, like a transfer 57 | * being returned or failing. 58 | */ 59 | const processPaymentEvent = async (event) => { 60 | console.log(`\n\nAnalyzing event: ${JSON.stringify(event)}`); 61 | const existingPayment = await db.getPaymentByPlaidId(event.transfer_id); 62 | 63 | if (!existingPayment) { 64 | console.warn( 65 | `Could not find a payment with ID ${event.transfer_id}. It might belong to another application` 66 | ); 67 | return; 68 | } 69 | console.log(`Found payment ${JSON.stringify(existingPayment)}`); 70 | 71 | const paymentId = existingPayment.id; 72 | const billId = existingPayment.bill_id; 73 | 74 | if (!event.event_type in PAYMENT_STATUS) { 75 | console.error(`Unknown event type ${event.event_type}`); 76 | return; 77 | } 78 | console.log( 79 | `The payment went from ${existingPayment.status} to ${event.event_type}!` 80 | ); 81 | 82 | if (EXPECTED_NEXT_STATES[existingPayment.status] == null) { 83 | console.error(`Hmm... existing payment has a status I don't recognize`); 84 | return; 85 | } 86 | if ( 87 | !EXPECTED_NEXT_STATES[existingPayment.status].includes(event.event_type) 88 | ) { 89 | // This doesn't normally happen; more likely it'll happen during development when 90 | // you (intentionally or accidentally) re-process the same batch of events 91 | console.error( 92 | `Not sure why a ${existingPayment.status} payment going to a ${event.event_type} state. Skipping` 93 | ); 94 | return; 95 | } 96 | console.log(`Updating the payment status to ${event.event_type}`); 97 | const errorMessage = event.failure_reason?.description ?? ""; 98 | await db.updatePaymentStatus( 99 | paymentId, 100 | event.event_type, 101 | billId, 102 | errorMessage 103 | ); 104 | }; 105 | 106 | module.exports = { syncPaymentData, PAYMENT_STATUS }; 107 | -------------------------------------------------------------------------------- /server/types.js: -------------------------------------------------------------------------------- 1 | 2 | // Just a couple of enum-like objects that we use to represent the status of 3 | // payments and bills. 4 | const PAYMENT_STATUS = { 5 | NEW: "new", 6 | INTENT_PENDING: "intent_pending", 7 | DENIED: "denied", 8 | PENDING: "pending", 9 | POSTED: "posted", 10 | SETTLED: "settled", 11 | FAILED: "failed", 12 | CANCELLED: "cancelled", 13 | RETURNED: "returned", 14 | }; 15 | 16 | const BILL_STATUS = { 17 | UNPAID: "unpaid", 18 | PAID: "paid", 19 | PAID_PENDING: "paid_pending", 20 | PARTIALLY_PAID: "partially_paid", 21 | }; 22 | 23 | module.exports = { PAYMENT_STATUS, BILL_STATUS }; 24 | -------------------------------------------------------------------------------- /server/utils.js: -------------------------------------------------------------------------------- 1 | const db = require("./db"); 2 | 3 | /** 4 | * Get the user ID of the currently logged-in user, which we do by looking 5 | * at the value of the `signedInUser` cookie. 6 | */ 7 | const getLoggedInUserId = function (req) { 8 | return req.cookies["signedInUser"]; 9 | }; 10 | 11 | /** 12 | * Fetch information about the currently signed-in user. 13 | */ 14 | const getUserObject = async function (userId) { 15 | const result = await db.getUserRecord(userId); 16 | return result; 17 | }; 18 | 19 | module.exports = { getLoggedInUserId, getUserObject }; 20 | -------------------------------------------------------------------------------- /server/webhookServer.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const express = require("express"); 3 | const bodyParser = require("body-parser"); 4 | const { syncPaymentData } = require("./syncPaymentData"); 5 | 6 | /** 7 | * Our server running on a different port that we'll use for handling webhooks. 8 | * We run this on a separate port so that it's easier to expose just this 9 | * server to the world using ngrok 10 | */ 11 | const WEBHOOK_PORT = process.env.WEBHOOK_PORT || 8001; 12 | 13 | const webhookApp = express(); 14 | webhookApp.use(bodyParser.urlencoded({ extended: false })); 15 | webhookApp.use(bodyParser.json()); 16 | 17 | const webhookServer = webhookApp.listen(WEBHOOK_PORT, function () { 18 | console.log( 19 | `Webhook receiver is up and running at http://localhost:${WEBHOOK_PORT}/` 20 | ); 21 | }); 22 | 23 | /** 24 | * This is our endpoint to receive webhooks from Plaid. We look at the 25 | * webhook_type (which represents the product) and then decide what function 26 | * to call from there. 27 | */ 28 | webhookApp.post("/server/receive_webhook", async (req, res, next) => { 29 | try { 30 | console.log("**INCOMING WEBHOOK**"); 31 | console.dir(req.body, { colors: true, depth: null }); 32 | const product = req.body.webhook_type; 33 | const code = req.body.webhook_code; 34 | // TODO (maybe): Verify webhook 35 | switch (product) { 36 | case "ITEM": 37 | handleItemWebhook(code, req.body); 38 | break; 39 | case "TRANSFER": 40 | handleTransferWebhook(code, req.body); 41 | break; 42 | default: 43 | console.log(`Can't handle webhook product ${product}`); 44 | break; 45 | } 46 | res.json({ status: "received" }); 47 | } catch (error) { 48 | next(error); 49 | } 50 | }); 51 | 52 | /** 53 | * Handle webhooks related to individual Items 54 | */ 55 | function handleItemWebhook(code, requestBody) { 56 | switch (code) { 57 | case "ERROR": 58 | console.log( 59 | `I received this error: ${requestBody.error.error_message}| should probably ask this user to connect to their bank` 60 | ); 61 | break; 62 | case "NEW_ACCOUNTS_AVAILABLE": 63 | console.log( 64 | `There are new accounts available at this Financial Institution! (Id: ${requestBody.item_id}) We may want to ask the user to share them with us` 65 | ); 66 | break; 67 | case "PENDING_EXPIRATION": 68 | case "PENDING_DISCONNECT": 69 | console.log( 70 | `We should tell our user to reconnect their bank with Plaid so there's no disruption to their service` 71 | ); 72 | break; 73 | case "USER_PERMISSION_REVOKED": 74 | console.log( 75 | `The user revoked access to this item. We should remove it from our records` 76 | ); 77 | break; 78 | case "WEBHOOK_UPDATE_ACKNOWLEDGED": 79 | console.log(`Hooray! You found the right spot!`); 80 | break; 81 | default: 82 | console.log(`Can't handle webhook code ${code}`); 83 | break; 84 | } 85 | } 86 | 87 | /** 88 | * Handle webhooks related to Transfers. The most important for our app is the 89 | * TRANSFER_EVENTS_UPDATE webhook, which tells us when the status of a transfer 90 | * has changed. (Note that the webhook doesn't tell us _which_ transfer has 91 | * changed, but that's okay. Calling /transfer/event/sync will give us the 92 | * latest status updates of all transfers.) 93 | */ 94 | 95 | function handleTransferWebhook(code, requestBody) { 96 | switch (code) { 97 | case "TRANSFER_EVENTS_UPDATE": 98 | console.log(`Looks like we have some new transfer events to process`); 99 | syncPaymentData(); 100 | break; 101 | case "RECURRING_NEW_TRANSFER": 102 | case "RECURRING_TRANSFER_SKIPPED": 103 | case "RECURRING_CANCELLED": 104 | console.log( 105 | `Received a ${code} event, which is weird because this app doesn't support recurring transfers` 106 | ); 107 | break; 108 | default: 109 | console.log(`Can't handle webhook code ${code}`); 110 | break; 111 | } 112 | } 113 | 114 | /** 115 | * Add in some basic error handling so our server doesn't crash if we run into 116 | * an error. 117 | */ 118 | const errorHandler = function (err, req, res, next) { 119 | console.error(`Your error:`); 120 | console.error(err); 121 | if (err.response?.data != null) { 122 | res.status(500).send(err.response.data); 123 | } else { 124 | res.status(500).send({ 125 | error_code: "OTHER_ERROR", 126 | error_message: "I got some other message on the server.", 127 | }); 128 | } 129 | }; 130 | webhookApp.use(errorHandler); 131 | 132 | const getWebhookServer = function () { 133 | return webhookServer; 134 | }; 135 | 136 | module.exports = { 137 | getWebhookServer, 138 | }; 139 | --------------------------------------------------------------------------------