├── .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 | Bill
27 |
28 |
29 |
30 | Original Amount
31 |
32 |
33 |
34 | Amount Paid
35 |
36 |
37 |
38 | Amount Pending
39 |
40 |
41 |
42 | Remaining
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Transfer UI
53 |
54 |
55 | Without Transfer UI
57 |
58 |
59 |
60 |
61 |
62 |
63 | Amount to pay
65 |
66 |
67 |
68 | Select an account
70 |
71 |
72 |
73 |
74 |
75 | Pay Bill
76 |
77 |
78 |
79 |
80 |
81 | Amount to pay
83 |
84 |
85 |
86 | Select an account
88 |
89 |
90 |
91 |
92 | Pay Bill
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | Date
103 | Amount
104 | Status(Hover for details)
105 | Dashboard
106 |
107 |
108 |
109 |
110 |
111 |
Debug area:
112 |
Perform Server Sync
113 |
Fire a webhook
114 |
115 |
116 |
Sign out
117 |
118 |
119 |
120 |
121 |
122 |
123 |
127 |
128 |
129 |
130 | From
131 |
132 |
133 |
134 | To
135 | Pay My Utility Bill
136 |
137 |
138 | Date
139 |
142 |
143 |
144 |
145 |
By clicking "Confirm", you authorize Pay My Utility Bill to make this transfer and
146 | agree to the Terms of Service
147 |
148 |
151 |
152 |
153 |
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 | Bill
24 | Date
25 | Amount Due
26 | Status
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Debug area:
34 |
Generate a new
35 | bill
36 |
37 |
38 |
Sign out
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 |
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 = `New account `;
22 | } else {
23 | const bankOptions = accountData.map(
24 | (account) =>
25 | `${account.bank_name} (${account.account_name}) `
26 | );
27 | innerHTML =
28 | bankOptions.join("\n") +
29 | `I'll choose another account `;
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) => `${userObj.username} `
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 |
--------------------------------------------------------------------------------