├── .gitignore
├── LICENSE
├── README.md
├── bill-bot-baggins
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── app
│ ├── (admin)
│ │ ├── (routes)
│ │ │ └── admin
│ │ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (auth)
│ │ └── (routes)
│ │ │ └── sign-in
│ │ │ └── [[...sign-in]]
│ │ │ └── page.tsx
│ ├── (client)
│ │ ├── (routes)
│ │ │ └── invoice
│ │ │ │ └── [invoiceId]
│ │ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (main)
│ │ └── (routes)
│ │ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ └── layout.tsx
├── components.json
├── components
│ ├── AdminAuth.tsx
│ ├── Columns.tsx
│ ├── DataTable.tsx
│ ├── Footer.tsx
│ ├── InvoiceDisplay.tsx
│ ├── InvoiceSection.tsx
│ ├── Navbar.tsx
│ ├── ParticleEffect.tsx
│ ├── Profile.tsx
│ ├── RecentSales.tsx
│ ├── TeamMember.tsx
│ ├── index.ts
│ ├── overview.tsx
│ └── ui
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── table.tsx
│ │ └── tabs.tsx
├── environment.d.ts
├── lib
│ ├── api.ts
│ ├── types.ts
│ ├── utils.ts
│ └── validations
│ │ └── form.ts
├── middleware.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── ales-nesetril-background.jpg
│ ├── archive
│ │ ├── big-logo.png
│ │ └── logo.png
│ ├── bg-pattern.jpeg
│ ├── big-logo.png
│ ├── dashboard-2.png
│ ├── github-mark-white.png
│ ├── invoice.png
│ ├── logo.png
│ ├── splashpage.png
│ └── up-down.svg
├── tailwind.config.ts
└── tsconfig.json
├── pull_request_template.md
├── salesforce-webhook
├── Dockerfile
├── eventHandler.js
├── package.json
├── routers
│ ├── authRouter.js
│ ├── salesforceRouter.js
│ └── stripeRouter.js
├── salesforce-pub-sub-api.mjs
└── server.mjs
└── stripe-webhook
├── Dockerfile
├── package-lock.json
├── package.json
├── route.js
└── stripe_salesforce_fetch.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 | /salesforce-webhook/.env
3 | /.env
4 | /stripe-webhook/.env
5 | # dependencies
6 | */node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # next.js
14 | /.next/
15 | /out/
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 | dump.rdb
24 | /.vscode
25 |
26 | # debug
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 | next-env.d.ts
40 |
41 | .env
42 | node_modules
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 OSLabs Beta
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
21 | [![Contributors][contributors-shield]][contributors-url]
22 | [![Forks][forks-shield]][forks-url]
23 | [![Stargazers][stars-shield]][stars-url]
24 | [![Issues][issues-shield]][issues-url]
25 | [![MIT License][license-shield]][license-url]
26 | [![LinkedIn][linkedin-shield]][linkedin-url]
27 |
28 |
29 |
30 |
31 |
49 |
50 |
51 |
52 |
53 |
54 | Table of Contents
55 |
56 |
57 | About The Project
58 |
61 |
62 |
63 | Getting Started
64 |
68 |
69 | Usage
70 | Roadmap
71 | Contributing
72 | License
73 | Contact
74 | Acknowledgments
75 |
76 |
77 |
78 |
79 |
80 |
81 | ## About The Project
82 |
83 | (back to top )
84 |
85 | ### Built With
86 |
87 | * [![NextJS][NextJs]][NextJS-url]
88 | * [![React][React.js]][React-url]
89 | * [![JavaScript][JavaScript]][JavaScript-url]
90 | * [![Typescript][TS.js]][TS-url]
91 | * [![Tailwind][Tailwind]][Tailwind-url]
92 | * [![][Git]][Git-url]
93 | * [![Jest][Jest]][Jest-url]
94 | * [![AWS][AWS]][AWS-url]
95 | * [![Docker][Docker]][Docker-url]
96 |
97 |
98 | (back to top )
99 |
100 |
101 |
102 |
103 | ## Getting Started
104 |
105 | ### Prerequisites
106 |
107 | Before using this application, make sure you have the following in place:
108 |
109 | 1. **Stripe Account:** You’ll need a Stripe account to connect to the payment gateway and manage donations.
110 | 2. **Salesforce Account:** You must have a Salesforce account with the necessary permissions to set up the integration.
111 | 3. **Web Server:** This application requires a web server to host the integration. You can use a cloud service like AWS, Google Cloud, or Heroku.
112 | 4. **API Keys:** Obtain API keys from both Stripe and Salesforce for secure communication between the systems.
113 |
114 | ### Installation
115 |
116 | 1. Get your publishable and secret Stripe API Keys at: [https://dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/apikeys)
117 | 2. Get your Salesforce access token by following the following steps: [https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5)
118 | 3. Clone the repo
119 | ```sh
120 | git clone https://github.com/oslabs-beta/PayStream.git
121 | ```
122 | 4. Install NPM packages
123 | ```sh
124 | npm install
125 | ```
126 | 5. Enter your JWT Secret into the `.env` file:
127 | ```js
128 | JWT_SECRET = 'ENTER YOUR JWT SECRET';
129 | ```
130 | 6. Enter your Salesforce Authentication information into the `.env` file:
131 | ```js
132 | SALESFORCE_GRAPHQL_URI = '[YOUR SALESFORCE INSTANCE]/services/data/v58.0/graphql'
133 | SALESFORCE_COOKIE_AUTH = 'ENTER YOUR SALEFORCE COOKING AUTHORIZATION'
134 | SALESFORCE_LOGIN_URL = 'ENTER YOUR SALESFORCE LOGIN URL'
135 | SALESFORCE_AUTH_TYPE = oauth-client-credentials
136 | SALESFORCE_USERNAME = 'ENTER YOUR SALESFORCE USERNAME'
137 | SALESFORCE_PASSWORD = 'ENTER YOUR SALESFORCE PASSWORD'
138 | SALESFORCE_ORG_ID = 'ENTER YOUR SALESFORCE ORGANIZATION ID'
139 | PUB_SUB_ENDPOINT = api.pubsub.salesforce.com:7443
140 | SALESFORCE_CLIENT_ID = 'ENTER YOUR SALESFORCE CLIENT ID'
141 | SALESFORCE_CLIENT_SECRET = 'ENTER YOUR SALESFORCE CLIENT SECRET'
142 | SALESFORCE_URL = 'https://test.salesforce.com'
143 | BASE64_PRIVATE_KEY = 'ENTER YOUR BASE64 PRIVATE KEY'
144 | ```
145 |
146 | 7. Enter your Stripe Authentication information into the `.env` file:
147 | ```js
148 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = 'ENTER YOUR STRIPE PUBLIC KEY';
149 | STRIPE_SECRET_KEY = 'ENTER YOUR STRIPE SECRET KEY';
150 | STRIPE_ENDPOINT_SECRET = 'ENTER YOUR STRIPE ENDPOINT SECRET'
151 | ```
152 |
153 | 8. Enter your Clerk Authentication information into the `.env` file:
154 | ```js
155 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = 'ENTER YOUR CLERK PUBLIC KEY';
156 | CLERK_SECRET_KEY = 'ENTER YOUR CLERK SECRET KEY';
157 |
158 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
159 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
160 | ```
161 |
162 | (back to top )
163 |
164 | ## Configuration
165 | 1. **Stripe Integration:**
166 | - Go to your Stripe dashboard and obtain your API keys.
167 | - Update the `.env` file with your Stripe API keys.
168 | - Go to [https://dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/webhooks) and generate your Stripe endpoint for your application.
169 | 2. **Salesforce Integration:**
170 | - Obtain Salesforce API credentials (Consumer Key, Consumer Secret, Username, and Password).
171 | - Update the `.env` file with your Salesforce API credentials.
172 | - The queries are mapped to custom payment records for a specific client and will need to be refactored to your application specific needs.
173 | 3. **Webhook Setup:**
174 | - Configure webhooks in Stripe to send events to your application’s endpoint.
175 | - Use a service like ngrok to create a secure tunnel to your local server or set up SSL on your production server.
176 | 4. **Customization:**
177 | - Modify the application to match your nonprofit’s specific Salesforce objects, fields, and donation processing logic.
178 |
179 |
180 | ## Screenshots
181 |
182 |
189 |
190 |
191 |
192 | ## Contributing
193 |
194 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
195 |
196 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
197 | Don't forget to give the project a star! Thanks again!
198 |
199 | 1. Fork the Project
200 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
201 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
202 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
203 | 5. Open a Pull Request
204 |
205 | (back to top )
206 |
207 |
208 |
209 |
210 | ## License
211 |
212 | Distributed under the MIT License. See `LICENSE.txt` for more information.
213 |
214 | (back to top )
215 |
216 |
217 |
218 |
219 | ## Contact
220 |
221 | Email Us: paystreamdevops@gmail.com
222 |
223 | Project Link: [https://github.com/oslabs-beta/PayStream](https://github.com/oslabs-beta/PayStream)
224 |
225 | (back to top )
226 |
227 |
228 |
229 | ## Authors
230 | | Developed By | Github | LinkedIn |
231 | | :----------: | :---------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------: |
232 | | Chandler Charity | [](https://github.com/lcchrty) | [](https://www.linkedin.com/in/chandlerchrty/) |
233 | | Julia Xin | [](https://github.com/juliazlx) |
234 | | Liam Hodges | [](https://github.com/lhodges3) | [](https://www.linkedin.com/in/liam-p-hodges/) |
235 | | Robert Hoover | [](https://github.com/Gambarou) | [](https://www.linkedin.com/in/roberthoover00/) |
236 |
237 |
238 |
239 | (back to top )
240 |
241 |
242 |
243 |
244 |
245 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/PayStream.svg?style=for-the-badge
246 | [contributors-url]: https://github.com/oslabs-beta/PayStream/graphs/contributors
247 | [forks-shield]: https://img.shields.io/github/forks/oslabs-beta/PayStream.svg?style=for-the-badge
248 | [forks-url]: https://github.com/oslabs-beta/PayStream/network/members
249 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/PayStream.svg?style=for-the-badge
250 | [stars-url]: https://github.com/oslabs-beta/PayStream/stargazers
251 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/PayStream.svg?style=for-the-badge
252 | [issues-url]: https://github.com/oslabs-beta/PayStream/issues
253 | [license-shield]: https://img.shields.io/github/license/oslabs-beta/PayStream.svg?style=for-the-badge
254 | [license-url]: https://github.com/oslabs-beta/PayStream/blob/master/LICENSE.txt
255 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
256 | [linkedin-url]: https://www.linkedin.com/company/pay-stream-dev
257 | [product-screenshot]: images/screenshot.png
258 | [React.js]: https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB
259 | [React-url]: https://reactjs.org/
260 | [TS.js]: https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white
261 | [TS-url]: https://www.typescriptlang.org/
262 | [JavaScript]: https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E
263 | [JavaScript-url]: https://www.javascript.com/
264 | [Jest]: https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white
265 | [Jest-url]: https://jestjs.io/
266 | [Git]: https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white
267 | [Git-url]: https://git-scm.com/
268 | [Tailwind]: https://img.shields.io/badge/Tailwind-%231DA1F2.svg?style=for-the-badge&logo=tailwind-css&logoColor=white
269 | [Tailwind-url]: https://tailwindcss.com/
270 | [NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
271 | [NextJS-url]: https://nextjs.org/
272 | [AWS]: https://img.shields.io/badge/AWS-%231E73BE.svg?style=for-the-badge&logo=amazon-aws&logoColor=white:
273 | [AWS-url]: https://aws.amazon.com/
274 | [Docker]: https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white
275 | [Docker-url]: https://www.docker.com/
276 |
--------------------------------------------------------------------------------
/bill-bot-baggins/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/bill-bot-baggins/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 | dump.rdb
22 | /.vscode
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | .env
40 |
--------------------------------------------------------------------------------
/bill-bot-baggins/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "semi": true,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "jsxSingleQuote": true,
7 | "plugins": ["prettier-plugin-tailwindcss"]
8 |
9 | }
--------------------------------------------------------------------------------
/bill-bot-baggins/app/(admin)/(routes)/admin/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from '@/components/ui/card';
9 | import { Overview, RecentSales, columns, DataTable } from '@/components/index';
10 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
11 | import { getRevenueData } from '@/lib/utils';
12 | import { getSalesForceAccessToken, getSalesForceInvoiceData } from '@/lib/api';
13 |
14 | async function AdminDashboardPage() {
15 | const currentYear = new Date().getFullYear().toString();
16 | // get SF accessToken
17 | const accessToken = await getSalesForceAccessToken();
18 | // get SF invoice data using accessToken
19 | const data = await getSalesForceInvoiceData(accessToken);
20 | // get formatted revenue data
21 | const revenueData = getRevenueData(data);
22 |
23 | const {
24 | payments,
25 | monthRevenue,
26 | yearRevenue,
27 | outstandingInvoices,
28 | revenueDataByMonth,
29 | monthRevenueGrowth,
30 | yearRevenueGrowth,
31 | } = revenueData;
32 |
33 | return (
34 |
35 |
39 |
40 |
41 | Overview
42 | Analytics
43 |
44 |
45 |
46 |
47 |
48 |
49 | Revenue YTD
50 |
51 |
61 |
62 |
63 |
64 |
65 | {`${yearRevenue.toLocaleString(
66 | 'en-US',
67 | {
68 | style: 'currency',
69 | currency: 'USD',
70 | }
71 | )}`}
72 |
73 | {yearRevenueGrowth >= 0 && '+'}
74 | {`${yearRevenueGrowth}`}% from last year
75 |
76 |
77 |
78 |
79 |
80 |
81 | Revenue MTD
82 |
83 |
93 |
94 |
95 |
96 |
97 | {`${monthRevenue.toLocaleString(
98 | 'en-US',
99 | {
100 | style: 'currency',
101 | currency: 'USD',
102 | }
103 | )}`}
104 |
105 | {monthRevenueGrowth >= 0 && '+'}
106 | {`${monthRevenueGrowth}% from last month`}
107 |
108 |
109 |
110 |
111 |
112 |
113 | # of Outstanding Invoices
114 |
115 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | {`${outstandingInvoices}`}
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | {`Overview of ${currentYear}`}
140 |
141 |
142 | {revenueDataByMonth ? (
143 |
144 | ) : (
145 |
146 | Error fetching data...
147 |
148 | )}
149 |
150 |
151 |
152 |
153 | Recent Payments
154 |
155 | There have been {payments.length} recent payments.
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | {data ? (
166 |
167 | ) : (
168 |
169 | Error fetching data...
170 |
171 | )}
172 |
173 |
174 |
175 | );
176 | }
177 |
178 | export default AdminDashboardPage;
179 |
--------------------------------------------------------------------------------
/bill-bot-baggins/app/(admin)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from '@/components/Navbar';
2 |
3 | export default function AdminLayout({
4 | children, // will be a page or nested layout
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 |
11 | {children}
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/bill-bot-baggins/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { AdminAuth } from '@/components/AdminAuth';
2 | import ParticleEffect from '@/components/ParticleEffect';
3 | import Image from 'next/image';
4 | import Link from 'next/link';
5 | import React from 'react';
6 |
7 | function SignInPage() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
28 | “PayStream: Where your payments journey begins, and
29 | financial adventures never feel like a quest.”
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Admin Login
40 |
41 |
42 | Enter your credentials below to sign in
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default SignInPage;
53 |
--------------------------------------------------------------------------------
/bill-bot-baggins/app/(client)/(routes)/invoice/[invoiceId]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stripe from 'stripe';
3 |
4 | import InvoiceDisplay from '@/components/InvoiceDisplay';
5 | import { InvoiceId } from '@/lib/types';
6 | import { getStripeInvoiceData } from '@/lib/api';
7 |
8 | async function InvoicePage({ params }: { params: InvoiceId }) {
9 | const { invoiceId } = params;
10 |
11 | const data = await getStripeInvoiceData(invoiceId);
12 |
13 | return (
14 |
15 | {data instanceof Stripe.errors.StripeError ? (
16 |
17 | ) : (
18 |
19 | )}
20 |
21 | );
22 | }
23 |
24 | export default InvoicePage;
25 |
--------------------------------------------------------------------------------
/bill-bot-baggins/app/(client)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Footer, Navbar } from '@/components/index';
2 |
3 | export default function ClientLayout({
4 | children, // will be a page or nested layout
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 |
11 | {children}
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/bill-bot-baggins/app/(main)/(routes)/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import Link from 'next/link';
3 | import React from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import TeamMember from '@/components/TeamMember';
6 |
7 | async function Home() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
24 |
25 |
Real-time updates,
26 |
Well-informed decisions
27 |
28 |
29 |
30 | PayStream provides real-time payment information so you can make
31 | well-informed business decisions based on your cash-flow metrics.
32 |
33 |
53 |
54 |
55 | Meet the T e a m
56 |
57 |
83 |
84 |
85 |
86 |
87 | );
88 | }
89 | export default Home;
90 |
--------------------------------------------------------------------------------
/bill-bot-baggins/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/app/favicon.ico
--------------------------------------------------------------------------------
/bill-bot-baggins/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 20 14.3% 4.1%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 20 14.3% 4.1%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 20 14.3% 4.1%;
13 | --primary: 24.6 95% 53.1%;
14 | --primary-foreground: 60 9.1% 97.8%;
15 | --secondary: 60 4.8% 95.9%;
16 | --secondary-foreground: 24 9.8% 10%;
17 | --muted: 60 4.8% 95.9%;
18 | --muted-foreground: 25 5.3% 44.7%;
19 | --accent: 60 4.8% 95.9%;
20 | --accent-foreground: 24 9.8% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 60 9.1% 97.8%;
23 | --border: 20 5.9% 90%;
24 | --input: 20 5.9% 90%;
25 | --ring: 24.6 95% 53.1%;
26 | --radius: 0.3rem;
27 | }
28 |
29 | .dark {
30 | --background: 20 14.3% 4.1%;
31 | --foreground: 60 9.1% 97.8%;
32 | --card: 20 14.3% 4.1%;
33 | --card-foreground: 60 9.1% 97.8%;
34 | --popover: 20 14.3% 4.1%;
35 | --popover-foreground: 60 9.1% 97.8%;
36 | --primary: 20.5 90.2% 48.2%;
37 | --primary-foreground: 60 9.1% 97.8%;
38 | --secondary: 12 6.5% 15.1%;
39 | --secondary-foreground: 60 9.1% 97.8%;
40 | --muted: 12 6.5% 15.1%;
41 | --muted-foreground: 24 5.4% 63.9%;
42 | --accent: 12 6.5% 15.1%;
43 | --accent-foreground: 60 9.1% 97.8%;
44 | --destructive: 0 72.2% 50.6%;
45 | --destructive-foreground: 60 9.1% 97.8%;
46 | --border: 12 6.5% 15.1%;
47 | --input: 12 6.5% 15.1%;
48 | --ring: 20.5 90.2% 48.2%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 | body {
57 | @apply bg-background text-foreground;
58 |
59 |
60 | }
61 | }
62 |
63 | body::-webkit-scrollbar {
64 | display: none; /* for Chrome, Safari, and Opera */
65 | }
66 |
67 |
68 | /* This will get rid of the default white background when you select an autofill for an input*/
69 | input:-webkit-autofill,
70 | input:-webkit-autofill:hover,
71 | input:-webkit-autofill:focus,
72 | input:-webkit-autofill:active{
73 | -webkit-text-fill-color: #ffffff;
74 | transition: background-color 5000s ease-in-out 0s;
75 | }
--------------------------------------------------------------------------------
/bill-bot-baggins/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '@/app/globals.css';
2 |
3 | import type { Metadata } from 'next';
4 | import { Open_Sans } from 'next/font/google';
5 | import { ClerkProvider } from '@clerk/nextjs';
6 | import { dark } from '@clerk/themes';
7 | const font = Open_Sans({ subsets: ['latin'] });
8 |
9 | export const metadata: Metadata = {
10 | title: 'PayStream',
11 | description: 'Generated by PLTG',
12 | };
13 |
14 | export default async function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode;
18 | }) {
19 | return (
20 |
25 |
26 |
27 |
28 |
29 | {children}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/bill-bot-baggins/components/AdminAuth.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 |
5 | import * as z from 'zod';
6 | import { zodResolver } from '@hookform/resolvers/zod';
7 | import { useSignIn } from '@clerk/nextjs';
8 | import { useRouter } from 'next/navigation';
9 | import { useForm } from 'react-hook-form';
10 | import { AlertTriangle } from 'lucide-react';
11 | import {
12 | Form,
13 | FormControl,
14 | FormField,
15 | FormItem,
16 | FormLabel,
17 | FormMessage,
18 | } from '@/components/ui/form';
19 | import { Input } from '@/components/ui/input';
20 | import { Button } from '@/components/ui/button';
21 | import { loginFormSchema } from '@/lib/validations/form';
22 | import { cn } from '@/lib/utils';
23 |
24 | interface UserAuthFormProps extends React.HTMLAttributes {}
25 |
26 | export function AdminAuth({ className, ...props }: UserAuthFormProps) {
27 | const { isLoaded, signIn, setActive } = useSignIn();
28 | const [isSubmitting, setIsSubmitting] = useState(false);
29 | const [isError, setIsError] = useState(false);
30 |
31 | const router = useRouter();
32 |
33 | async function onSubmit(values: z.infer) {
34 | // clerk provided function
35 | if (!isLoaded) {
36 | return null;
37 | }
38 | // clears error warning when a user re-submits a different username or password
39 | setIsError(false);
40 |
41 | setIsSubmitting(true);
42 |
43 | // attempts to login a user in Clerk with provided credentials
44 | try {
45 | const result = await signIn.create({
46 | identifier: values.email,
47 | password: values.password,
48 | });
49 |
50 | // If successful sets an active session and redirects to /admin
51 | if (result.status === 'complete') {
52 | await setActive({ session: result.createdSessionId });
53 | router.push('/admin');
54 | // not sure about the else here, will revisit
55 | } else {
56 | console.log(result);
57 | }
58 | // if there was an error logging in (wrong password/email)
59 | // sets an error so that it will be displayed to the user
60 | } catch (err: any) {
61 | console.log(err);
62 | setIsSubmitting(false);
63 | setIsError(true);
64 | }
65 | }
66 |
67 | // zod form validation, makes sure the user has inputted a proper email
68 | // and that the password is at least 8 chars long
69 | const form = useForm>({
70 | resolver: zodResolver(loginFormSchema),
71 | defaultValues: {
72 | email: '',
73 | password: '',
74 | },
75 | });
76 |
77 | return (
78 |
79 | {isError && (
80 |
81 |
82 |
Invalid username or password. Please try again.
83 |
84 | )}
85 |
132 |
133 |
134 | );
135 | }
136 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/Columns.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ColumnDef } from '@tanstack/react-table';
4 | import { PaymentProps } from '@/lib/types';
5 | import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
6 |
7 | export const columns: ColumnDef[] = [
8 | {
9 | accessorKey: 'invoice_id',
10 | header: ({ column }) => {
11 | return (
12 | column.toggleSorting()}
15 | >
16 | Invoice ID
17 | {column.getIsSorted() === 'asc' ? (
18 |
19 | ) : column.getIsSorted() === 'desc' ? (
20 |
21 | ) : (
22 |
23 | )}
24 |
25 | );
26 | },
27 | },
28 | {
29 | accessorKey: 'account_name',
30 | header: ({ column }) => {
31 | return (
32 | column.toggleSorting()}
35 | >
36 | Client
37 | {column.getIsSorted() === 'asc' ? (
38 |
39 | ) : column.getIsSorted() === 'desc' ? (
40 |
41 | ) : (
42 |
43 | )}
44 |
45 | );
46 | },
47 | },
48 | {
49 | accessorKey: 'project_name',
50 | header: ({ column }) => {
51 | return (
52 | column.toggleSorting()}
55 | >
56 | Project
57 | {column.getIsSorted() === 'asc' ? (
58 |
59 | ) : column.getIsSorted() === 'desc' ? (
60 |
61 | ) : (
62 |
63 | )}
64 |
65 | );
66 | },
67 | },
68 | {
69 | accessorKey: 'amount',
70 | header: ({ column }) => {
71 | return (
72 | column.toggleSorting()}
75 | >
76 | Amount
77 | {column.getIsSorted() === 'asc' ? (
78 |
79 | ) : column.getIsSorted() === 'desc' ? (
80 |
81 | ) : (
82 |
83 | )}
84 |
85 | );
86 | },
87 | cell: ({ row }) => {
88 | const amount = parseFloat(row.getValue('amount'));
89 | const formatted = new Intl.NumberFormat('en-US', {
90 | style: 'currency',
91 | currency: 'USD',
92 | }).format(amount);
93 |
94 | return {formatted}
;
95 | },
96 | },
97 | {
98 | accessorKey: 'invoice_sent_date',
99 | header: ({ column }) => {
100 | return (
101 | column.toggleSorting()}
104 | >
105 | Invoice Sent Date
106 | {column.getIsSorted() === 'asc' ? (
107 |
108 | ) : column.getIsSorted() === 'desc' ? (
109 |
110 | ) : (
111 |
112 | )}
113 |
114 | );
115 | },
116 | },
117 | {
118 | accessorKey: 'invoice_due_date',
119 | header: ({ column }) => {
120 | return (
121 | column.toggleSorting()}
124 | >
125 | Invoice Due Date
126 | {column.getIsSorted() === 'asc' ? (
127 |
128 | ) : column.getIsSorted() === 'desc' ? (
129 |
130 | ) : (
131 |
132 | )}
133 |
134 | );
135 | },
136 | },
137 | {
138 | accessorKey: 'payment_date',
139 | header: ({ column }) => {
140 | return (
141 | column.toggleSorting()}
144 | >
145 | Payment Date
146 | {column.getIsSorted() === 'asc' ? (
147 |
148 | ) : column.getIsSorted() === 'desc' ? (
149 |
150 | ) : (
151 |
152 | )}
153 |
154 | );
155 | },
156 | },
157 | {
158 | accessorKey: 'payment_method',
159 | header: 'Payment Method',
160 | },
161 | ];
162 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/DataTable.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | ColumnDef,
5 | ColumnFiltersState,
6 | PaginationState,
7 | SortingState,
8 | flexRender,
9 | getCoreRowModel,
10 | getFilteredRowModel,
11 | getPaginationRowModel,
12 | getSortedRowModel,
13 | useReactTable,
14 | } from '@tanstack/react-table';
15 |
16 | import {
17 | Table,
18 | TableBody,
19 | TableCell,
20 | TableHead,
21 | TableHeader,
22 | TableRow,
23 | } from '@/components/ui/table';
24 | import { Button } from '@/components/ui/button';
25 | import { Input } from '@/components/ui/input';
26 | import { useMemo, useState } from 'react';
27 |
28 | type DataTableProps = {
29 | columns: ColumnDef[];
30 | data: TData[];
31 | };
32 |
33 | export default function DataTable({
34 | columns,
35 | data,
36 | }: DataTableProps) {
37 | const [sorting, setSorting] = useState([]);
38 | const [columnFilters, setColumnFilters] = useState([]);
39 |
40 | const [{ pageIndex, pageSize }, setPagination] = useState({
41 | pageIndex: 0,
42 | pageSize: 8,
43 | });
44 |
45 | const pagination = useMemo(
46 | () => ({
47 | pageIndex,
48 | pageSize,
49 | }),
50 | [pageIndex, pageSize]
51 | );
52 |
53 | const table = useReactTable({
54 | data,
55 | columns,
56 | onPaginationChange: setPagination,
57 | getCoreRowModel: getCoreRowModel(),
58 | onSortingChange: setSorting,
59 | getPaginationRowModel: getPaginationRowModel(),
60 | getSortedRowModel: getSortedRowModel(),
61 | onColumnFiltersChange: setColumnFilters,
62 | getFilteredRowModel: getFilteredRowModel(),
63 | state: {
64 | sorting,
65 | columnFilters,
66 | pagination,
67 | },
68 | });
69 |
70 | return (
71 | <>
72 |
73 |
74 |
80 | table.getColumn('invoice_id')?.setFilterValue(event.target.value)
81 | }
82 | className='max-w-sm'
83 | />
84 |
85 |
86 |
87 |
94 | table
95 | .getColumn('account_name')
96 | ?.setFilterValue(event.target.value)
97 | }
98 | className='max-w-sm'
99 | />
100 |
101 |
102 |
setColumnFilters([])}
107 | >
108 | Clear
109 |
110 |
111 |
112 |
113 |
114 | {table.getHeaderGroups().map((headerGroup) => (
115 |
116 | {headerGroup.headers.map((header) => {
117 | return (
118 |
119 | {header.isPlaceholder
120 | ? null
121 | : flexRender(
122 | header.column.columnDef.header,
123 | header.getContext()
124 | )}
125 |
126 | );
127 | })}
128 |
129 | ))}
130 |
131 |
132 | {table.getRowModel().rows?.length ? (
133 | table.getRowModel().rows.map((row) => (
134 |
138 | {row.getVisibleCells().map((cell) => (
139 |
140 | {flexRender(
141 | cell.column.columnDef.cell,
142 | cell.getContext()
143 | )}
144 |
145 | ))}
146 |
147 | ))
148 | ) : (
149 |
150 |
154 | No results.
155 |
156 |
157 | )}
158 |
159 |
160 |
161 |
162 |
table.previousPage()}
165 | disabled={!table.getCanPreviousPage()}
166 | >
167 | Previous
168 |
169 |
table.nextPage()}
172 | disabled={!table.getCanNextPage()}
173 | >
174 | Next
175 |
176 |
177 | Page
178 |
179 | {table.getState().pagination.pageIndex + 1} of{' '}
180 | {table.getPageCount()}
181 |
182 |
183 |
184 | >
185 | );
186 | }
187 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Footer() {
4 | return (
5 |
6 |
7 | © 2023 PLTG. All rights reserved.
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/InvoiceDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stripe from 'stripe';
3 | import InvoiceSection from '@/components/InvoiceSection';
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from '@/components/ui/card';
12 | import { Button } from '@/components/ui/button';
13 | import { Badge } from '@/components/ui/badge';
14 | import { AlertCircle } from 'lucide-react';
15 |
16 | export default function InvoiceDisplay({
17 | invoice,
18 | error,
19 | }: {
20 | invoice: Stripe.Response | undefined;
21 | error?: string;
22 | }) {
23 | let invoiceDate: string, dueDate: string;
24 |
25 | if (invoice) {
26 | const created = new Date(invoice.created * 1000);
27 | invoiceDate = created.toLocaleString(undefined, {
28 | month: 'short',
29 | day: 'numeric',
30 | year: 'numeric',
31 | });
32 |
33 | const due = new Date((invoice.due_date as number) * 1000);
34 | dueDate = due.toLocaleString(undefined, {
35 | month: 'short',
36 | day: 'numeric',
37 | year: 'numeric',
38 | });
39 | }
40 | return invoice && !error ? (
41 |
42 |
43 | Invoice
44 | {invoice.status === 'open' ? (
45 |
46 | {invoice.status}
47 |
48 | ) : invoice.status === 'paid' ? (
49 |
50 | {invoice.status}
51 |
52 | ) : (
53 |
54 | {invoice.status}
55 |
56 | )}
57 |
58 |
59 |
60 |
61 | #{invoice.number}
62 |
63 |
64 |
65 | Executive Service Corps{' '}
66 |
67 | 1000 Alameda St
68 |
69 |
70 | Los Angeles, CA 90012
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
83 |
84 |
85 |
86 |
90 | {invoice.customer_address && (
91 |
92 | {invoice.customer_address.line1}
93 | {invoice.customer_address.line2}
94 |
95 | {invoice.customer_address.city},{' '}
96 | {invoice.customer_address.state} {' '}
97 | {invoice.customer_address.postal_code}
98 |
99 |
100 | )}
101 |
102 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
116 |
117 |
118 |
123 |
133 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | {invoice.status === 'open' ? (
151 |
152 | Pay Invoice
153 |
154 | ) : (
155 |
156 | Pay Invoice
157 |
158 | )}
159 |
163 | Download Invoice
164 |
165 |
166 |
167 |
168 |
169 | ) : (
170 |
171 |
172 |
173 |
174 |
175 |
176 | Error
177 |
178 |
179 | Invalid payment link. Please check the link and try again.
180 |
181 |
182 |
183 |
184 |
185 | );
186 | }
187 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/InvoiceSection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | type InvoiceSectionProps = {
5 | title: string;
6 | invoiceData: string | number | null;
7 | variant?: boolean;
8 | };
9 |
10 | function InvoiceSection({ title, invoiceData, variant }: InvoiceSectionProps) {
11 | return (
12 |
13 |
{title}
14 |
{invoiceData}
15 |
16 | );
17 | }
18 |
19 | export default InvoiceSection;
20 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import Image from 'next/image';
4 | import Profile from './Profile';
5 | import { auth } from '@clerk/nextjs';
6 |
7 | const Navbar = async () => {
8 | const user = auth();
9 | return (
10 |
11 |
12 |
13 |
22 |
23 |
24 | {user ? : null}
25 |
26 | );
27 | };
28 |
29 | export default Navbar;
30 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ParticleEffect.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useCallback } from 'react';
4 | import type { Container, Engine } from 'tsparticles-engine';
5 | import Particles from 'react-particles';
6 | //import { loadFull } from "tsparticles"; // if you are going to use `loadFull`, install the "tsparticles" package too.
7 | import { loadSlim } from 'tsparticles-slim'; // if you are going to use `loadSlim`, install the "tsparticles-slim" package too.
8 |
9 | const ParticleEffect = () => {
10 | const particlesInit = useCallback(async (engine: Engine) => {
11 | console.log(engine);
12 |
13 | // you can initialize the tsParticles instance (engine) here, adding custom shapes or presets
14 | // this loads the tsparticles package bundle, it's the easiest method for getting everything ready
15 | // starting from v2 you can add only the features you need reducing the bundle size
16 | //await loadFull(engine);
17 | await loadSlim(engine);
18 | }, []);
19 |
20 | const particlesLoaded = useCallback(
21 | async (container: Container | undefined) => {
22 | await console.log(container);
23 | },
24 | []
25 | );
26 | return (
27 |
101 | );
102 | };
103 |
104 | export default ParticleEffect;
105 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { UserButton } from '@clerk/nextjs';
2 | import React from 'react';
3 |
4 | function Profile() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
12 | export default Profile;
13 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/RecentSales.tsx:
--------------------------------------------------------------------------------
1 | import { PaymentProps } from '@/lib/types';
2 |
3 | export default function RecentSales({
4 | payments,
5 | }: {
6 | payments: PaymentProps[];
7 | }) {
8 | return (
9 |
10 | {payments.map((payment) => (
11 |
12 |
13 |
14 | {payment.account_name}
15 |
16 |
17 | ESC-{payment.invoice_id}
18 |
19 |
20 |
21 | +
22 | {payment.amount.toLocaleString('en-US', {
23 | style: 'currency',
24 | currency: 'USD',
25 | })}
26 |
27 |
28 | ))}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/TeamMember.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import Link from 'next/link';
3 | import React from 'react';
4 |
5 | type TeamMemberProps = {
6 | href: string;
7 | email: string;
8 | firstName: string;
9 | lastName: string;
10 | };
11 |
12 | const TeamMember = ({ href, email, firstName, lastName }: TeamMemberProps) => {
13 | return (
14 |
15 |
16 |
23 |
24 |
25 |
26 | {firstName} {' '}
27 | {lastName}{' '}
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default TeamMember;
35 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/index.ts:
--------------------------------------------------------------------------------
1 | import Navbar from '@/components/Navbar';
2 | import Footer from '@/components/Footer';
3 | import Overview from '@/components/overview';
4 | import RecentSales from '@/components/RecentSales';
5 | import DataTable from '@/components/DataTable';
6 | import { columns }from '@/components/Columns';
7 |
8 |
9 | export {
10 | Navbar,
11 | Footer,
12 | Overview,
13 | RecentSales,
14 | DataTable,
15 | columns,
16 | };
--------------------------------------------------------------------------------
/bill-bot-baggins/components/overview.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, useState } from 'react';
4 | import {
5 | Bar,
6 | BarChart,
7 | ResponsiveContainer,
8 | Tooltip,
9 | TooltipProps,
10 | XAxis,
11 | YAxis,
12 | } from 'recharts';
13 |
14 | import type {
15 | ValueType,
16 | NameType,
17 | } from 'recharts/types/component/DefaultTooltipContent';
18 |
19 | const CustomTooltip = ({
20 | active,
21 | payload,
22 | label,
23 | }: TooltipProps) => {
24 | if (active && payload && payload.length) {
25 | return (
26 |
27 |
{`Revenue`}
28 |
{`${label}: ${payload[0].value?.toLocaleString(
29 | 'en-US',
30 | {
31 | style: 'currency',
32 | currency: 'USD',
33 | }
34 | )}`}
35 |
36 | );
37 | }
38 |
39 | return null;
40 | };
41 |
42 | type OverviewProps = {
43 | data: {
44 | month: string;
45 | revenue: number;
46 | }[];
47 | };
48 |
49 | const Overview = ({ data }: OverviewProps) => {
50 | const [isLoading, setIsLoading] = useState(true);
51 |
52 | useEffect(() => {
53 | setIsLoading(false);
54 | }, [data]);
55 |
56 | return !isLoading ? (
57 |
58 |
59 |
66 | `$${value}`}
72 | />
73 | } />
74 |
75 |
76 |
77 | ) : (
78 |
79 |
83 |
84 | Loading...
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default Overview;
92 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | }
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | indigo:
22 | 'bg-indigo-600 hover:bg-indigo-600/90 w-full transition-all active:scale-95',
23 | },
24 | size: {
25 | default: 'h-10 px-4 py-2',
26 | sm: 'h-9 rounded-md px-3',
27 | lg: 'h-11 rounded-md px-8',
28 | icon: 'h-10 w-10',
29 | },
30 | },
31 | defaultVariants: {
32 | variant: 'default',
33 | size: 'default',
34 | },
35 | }
36 | );
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean;
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : 'button';
47 | return (
48 |
53 | );
54 | }
55 | );
56 | Button.displayName = 'Button';
57 |
58 | export { Button, buttonVariants };
59 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 |
48 | ))
49 | TableFooter.displayName = "TableFooter"
50 |
51 | const TableRow = React.forwardRef<
52 | HTMLTableRowElement,
53 | React.HTMLAttributes
54 | >(({ className, ...props }, ref) => (
55 |
63 | ))
64 | TableRow.displayName = "TableRow"
65 |
66 | const TableHead = React.forwardRef<
67 | HTMLTableCellElement,
68 | React.ThHTMLAttributes
69 | >(({ className, ...props }, ref) => (
70 |
78 | ))
79 | TableHead.displayName = "TableHead"
80 |
81 | const TableCell = React.forwardRef<
82 | HTMLTableCellElement,
83 | React.TdHTMLAttributes
84 | >(({ className, ...props }, ref) => (
85 |
90 | ))
91 | TableCell.displayName = "TableCell"
92 |
93 | const TableCaption = React.forwardRef<
94 | HTMLTableCaptionElement,
95 | React.HTMLAttributes
96 | >(({ className, ...props }, ref) => (
97 |
102 | ))
103 | TableCaption.displayName = "TableCaption"
104 |
105 | export {
106 | Table,
107 | TableHeader,
108 | TableBody,
109 | TableFooter,
110 | TableHead,
111 | TableRow,
112 | TableCell,
113 | TableCaption,
114 | }
115 |
--------------------------------------------------------------------------------
/bill-bot-baggins/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TabsPrimitive from '@radix-ui/react-tabs';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/bill-bot-baggins/environment.d.ts:
--------------------------------------------------------------------------------
1 | namespace NodeJS {
2 | interface ProcessEnv {
3 | STRIPE_SECRET_KEY: string;
4 | JWT_SECRET: Secret;
5 | BASE64_PRIVATE_KEY: string;
6 | TEST_CLIENT_ID: string;
7 | TEST_USERNAME: string;
8 | TEST_URL: string;
9 | }
10 | }
--------------------------------------------------------------------------------
/bill-bot-baggins/lib/api.ts:
--------------------------------------------------------------------------------
1 | import Stripe from 'stripe';
2 | import { InvoiceProps, PaymentProps } from '@/lib/types';
3 | import axios, { AxiosError } from 'axios';
4 | import { getToken } from 'sf-jwt-token';
5 |
6 | /*
7 | overview of API field names and what they reference
8 | Opportunity_18_Digit_ID__c: 18 character unique ID
9 | npe01__Opportunity__c: references opportunity by name from the payment record
10 | ID: unique reference id for the object
11 | Invoice_Sent_Date__c: date sent
12 | Invoice__c: Invoice number
13 | Opportunity_Account_Name__c: account name of the opportunity (project) the payment is for
14 | npe01__Payment_Amount__c: amount on invoice
15 | npe01__Payment_Date__c: date invoice paid
16 | npe01__Payment_Method__c: method of payment (cc, check, etc.)
17 | */
18 | const { SALESFORCE_CLIENT_ID, SALESFORCE_USERNAME, SALESFORCE_URL, BASE64_PRIVATE_KEY, STRIPE_SECRET_KEY, SALESFORCE_GRAPHQL_URI, SALESFORCE_COOKIE_AUTH } = process.env;
19 | const data = JSON.stringify({
20 | query: `query payments {
21 | uiapi {
22 | query {
23 | npe01__OppPayment__c {
24 | edges {
25 | node {
26 | Id
27 | Invoice__c {
28 | value
29 | }
30 | Invoice_Sent_Date__c {
31 | value
32 | }
33 | npe01__Payment_Amount__c {
34 | value
35 | }
36 | Opportunity_Account_Name__c {
37 | value
38 | }
39 | Opportunity_18_Digit_ID__c {
40 | value
41 | }
42 | Project_Number__c {
43 | value
44 | }
45 | npe01__Payment_Method__c {
46 | value
47 | }
48 | npe01__Payment_Date__c {
49 | value
50 | }
51 | npe01__Scheduled_Date__c{
52 | value
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }`,
60 | variables: {}
61 | });
62 |
63 | export const getStripeInvoiceData = async (invoiceId: string) => {
64 | const stripe = new Stripe(STRIPE_SECRET_KEY, {
65 | apiVersion: '2023-08-16',
66 | typescript: true,
67 | });
68 | try {
69 | const invoice = await stripe.invoices.retrieve(invoiceId);
70 | return invoice;
71 | } catch (err) {
72 | if (err instanceof Stripe.errors.StripeError) {
73 | if (err.type === 'StripeInvalidRequestError') {
74 | return err
75 | }
76 | }
77 | }
78 | }
79 |
80 | export const getSalesForceInvoiceData = async (accessToken: string) => {
81 | try {
82 | const res = await axios.request({
83 | method: 'post',
84 | maxBodyLength: Infinity,
85 | url: SALESFORCE_GRAPHQL_URI,
86 | headers: {
87 | 'X-Chatter-Entity-Encoding': 'false',
88 | 'Content-Type': 'application/json',
89 | 'Authorization': `Bearer ${accessToken}`,
90 | 'Cookie': SALESFORCE_COOKIE_AUTH,
91 | },
92 | data: data,
93 | });
94 |
95 | // retrieve all invoice information from salesforce graphql call
96 | const paymentInfo = res.data.data.uiapi.query.npe01__OppPayment__c.edges;
97 |
98 | const invoices: PaymentProps[] = [];
99 |
100 | paymentInfo.map((record: InvoiceProps) => {
101 | const { Id, Invoice__c, Invoice_Sent_Date__c, npe01__Payment_Amount__c, Opportunity_Account_Name__c, Project_Number__c, npe01__Payment_Method__c, npe01__Payment_Date__c, npe01__Scheduled_Date__c } = record.node
102 |
103 | const invoice: PaymentProps = {
104 | sf_unique_id: Id,
105 | invoice_id: Invoice__c.value,
106 | amount: npe01__Payment_Amount__c.value,
107 | invoice_sent_date: Invoice_Sent_Date__c.value,
108 | payment_date: npe01__Payment_Date__c.value,
109 | invoice_due_date: npe01__Scheduled_Date__c.value,
110 | payment_method: npe01__Payment_Method__c.value,
111 | project_name: Project_Number__c.value,
112 | account_name: Opportunity_Account_Name__c.value,
113 | }
114 |
115 | invoices.push(invoice);
116 |
117 | })
118 | return invoices
119 | } catch (err) {
120 | if (err instanceof AxiosError) {
121 | console.log(err)
122 | }
123 | }
124 |
125 | }
126 |
127 | export const getSalesForceAccessToken = async () => {
128 | // convert the base64 private key into a string
129 | const privateKey = Buffer.from(BASE64_PRIVATE_KEY, 'base64').toString('utf8');
130 |
131 | try {
132 | // gets a new (if it hasn't expired it will send the still active token) access token from sales force
133 | const jwtTokenResponse = await getToken({
134 | iss: SALESFORCE_CLIENT_ID as string,
135 | sub: SALESFORCE_USERNAME as string,
136 | aud: SALESFORCE_URL as string,
137 | privateKey: privateKey,
138 | });
139 |
140 | return jwtTokenResponse.access_token;
141 | }
142 | catch (error) {
143 | if (error instanceof Error) {
144 | console.error('Error fetching Salesforce access token:', {
145 | name: error.name,
146 | message: error.message,
147 | stack: error.stack
148 | });
149 | }
150 | throw Error
151 | }
152 | }
--------------------------------------------------------------------------------
/bill-bot-baggins/lib/types.ts:
--------------------------------------------------------------------------------
1 | export type PaymentProps = {
2 | sf_unique_id: string;
3 | account_name?: string;
4 | project_name?: string;
5 | payment_method?: string;
6 | invoice_sent_date?: string;
7 | invoice_due_date?: string;//due 14 days from invoice date
8 | payment_date?: string; //invoice_paid_date
9 | amount: number;
10 | invoice_id: string;
11 | }
12 |
13 | export type InvoiceId = {
14 | invoiceId: string
15 | }
16 |
17 | export type Token = {
18 | token: string;
19 | };
20 |
21 | export type InvoiceProps = {
22 | node: {
23 | Id: string,
24 | Invoice__c: {
25 | value: string
26 | },
27 | Project_Number__c: {
28 | value: string | undefined
29 | }
30 | Invoice_Sent_Date__c: {
31 | value: string | undefined
32 | },
33 | npe01__Payment_Amount__c: {
34 | value: number
35 | },
36 | Opportunity_Account_Name__c: {
37 | value: string
38 | },
39 | npe01__Payment_Method__c: {
40 | value: string
41 | },
42 | npe01__Payment_Date__c: {
43 | value: string | undefined
44 | },
45 | npe01__Scheduled_Date__c: {
46 | value: string | undefined
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/bill-bot-baggins/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 | import { PaymentProps } from "./types";
4 |
5 | export const getMonthlyRevenueData = (data: PaymentProps[] ) => {
6 | // initialize the revenue data for each month to zero
7 | const revenueByMonth = [
8 | { month: 'Jan', revenue: 0 },
9 | { month: 'Feb', revenue: 0 },
10 | { month: 'Mar', revenue: 0 },
11 | { month: 'Apr', revenue: 0 },
12 | { month: 'May', revenue: 0 },
13 | { month: 'Jun', revenue: 0 },
14 | { month: 'Jul', revenue: 0 },
15 | { month: 'Aug', revenue: 0 },
16 | { month: 'Sep', revenue: 0 },
17 | { month: 'Oct', revenue: 0 },
18 | { month: 'Nov', revenue: 0 },
19 | { month: 'Dec', revenue: 0 },
20 | ];
21 | // loop through the resulting data from salesforce and add the revenue data to the specific month
22 | data.forEach((invoice) => {
23 | const { payment_date, amount } = invoice;
24 | const currentYear = new Date().getFullYear().toString();
25 |
26 | if (payment_date && payment_date?.includes(currentYear)) {
27 | const month = Number(payment_date.slice(5, 7));
28 | revenueByMonth[month - 1].revenue += amount;
29 | }
30 | });
31 |
32 | return revenueByMonth;
33 | };
34 |
35 | export function cn(...inputs: ClassValue[]) {
36 | return twMerge(clsx(inputs))
37 | }
38 |
39 | type ResultsProps = {
40 | revenueData: number | null,
41 | payments: PaymentProps[] | any[]
42 | yearRevenue: number,
43 | outstandingInvoices: number,
44 | monthRevenue: number,
45 | monthRevenueGrowth: number,
46 | yearRevenueGrowth: number,
47 | revenueDataByMonth: {
48 | month: string;
49 | revenue: number;
50 | }[] | undefined
51 | }
52 |
53 | export const getRevenueData = (data: PaymentProps[] | undefined) => {
54 |
55 | const results: ResultsProps = {
56 | revenueData: null,
57 | payments: [],
58 | yearRevenue: 0,
59 | outstandingInvoices: 0,
60 | monthRevenue: 0,
61 | monthRevenueGrowth: 0,
62 | yearRevenueGrowth: 0,
63 | revenueDataByMonth: undefined
64 | }
65 |
66 | let pastMonthRevenue = 0;
67 | let pastYearRevenue = 0;
68 | const currentYear = new Date().getFullYear().toString();
69 | const pastYear = (new Date().getFullYear() - 1).toString();
70 | const currentMonth = (new Date().getMonth() + 1).toString();
71 | const pastMonth = new Date().getMonth().toString();
72 | const currentDate = new Date();
73 |
74 | sortDataByPaymentDate(data);
75 |
76 | if (data) {
77 | // get 5 most recent payments (array is now sorted by most recent payment)
78 | for (let i = 0; i < data.length && i < 5; i++) {
79 | if (data[i].payment_date && data[i].payment_date?.includes(currentYear)) {
80 | results.payments.push(data[i]);
81 | }
82 | }
83 |
84 | data.forEach((invoice) => {
85 | if (invoice.payment_date && invoice.payment_date.includes(currentYear)) {
86 | results.yearRevenue += invoice.amount;
87 | }
88 | if (invoice.payment_date && invoice.payment_date.includes(pastYear)) {
89 | pastYearRevenue += invoice.amount;
90 | }
91 | if (
92 | invoice.payment_date &&
93 | invoice.payment_date.slice(5, 7).includes(currentMonth)
94 | ) {
95 | results.monthRevenue += invoice.amount;
96 | }
97 | if (
98 | invoice.payment_date &&
99 | invoice.payment_date.slice(5, 7).includes(pastMonth)
100 | ) {
101 | pastMonthRevenue += invoice.amount;
102 | }
103 | if (
104 | invoice.invoice_due_date &&
105 | new Date(invoice.invoice_due_date) < currentDate &&
106 | invoice.payment_date === undefined
107 | ) {
108 | results.outstandingInvoices += 1;
109 | }
110 | });
111 | results.revenueDataByMonth = getMonthlyRevenueData(data);
112 |
113 | results.monthRevenueGrowth = getRevenueGrowth(pastMonthRevenue, results.monthRevenue);
114 |
115 | results.yearRevenueGrowth = getRevenueGrowth(pastYearRevenue, results.yearRevenue);
116 |
117 | }
118 | return (results)
119 | }
120 |
121 | const getRevenueGrowth = (pastRevenue: number, currentRevenue: number) => {
122 | return pastRevenue !== 0
123 | ? (currentRevenue - pastRevenue) / pastRevenue * 100
124 | : currentRevenue === 0
125 | ? 0
126 | : 100;
127 | }
128 |
129 | const sortDataByPaymentDate = (paymentArray: PaymentProps[] | undefined) => {
130 |
131 | // sort the data array by payment_date, which allows for the 5 most recent sales to be shown
132 | if (paymentArray) {
133 | paymentArray.sort((a, b) => {
134 | const dateA = a.payment_date ? new Date(a.payment_date).getTime() : 0;
135 | const dateB = b.payment_date ? new Date(b.payment_date).getTime() : 0;
136 |
137 | return dateB - dateA;
138 | });
139 | }
140 | }
--------------------------------------------------------------------------------
/bill-bot-baggins/lib/validations/form.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const loginFormSchema = z.object({
4 | email: z.string().email(),
5 | password: z.string().min(8, { message: "Password must contain at least 8 characters"}),
6 | })
--------------------------------------------------------------------------------
/bill-bot-baggins/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@clerk/nextjs";
2 |
3 | // This example protects all routes including api/trpc routes
4 | // Please edit this to allow other routes to be public as needed.
5 | // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
6 | export default authMiddleware({
7 | publicRoutes: ["/", "/invoice/:invoiceId", "/api/invoice"]
8 | });
9 |
10 | export const config = {
11 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)","/","/(api|trpc)(.*)"],
12 | };
13 |
--------------------------------------------------------------------------------
/bill-bot-baggins/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | };
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/bill-bot-baggins/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "paystream",
3 | "version": "0.9.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@clerk/nextjs": "^4.26.1",
13 | "@clerk/themes": "^1.7.5",
14 | "@hookform/resolvers": "^3.3.2",
15 | "@radix-ui/react-alert-dialog": "^1.0.5",
16 | "@radix-ui/react-dropdown-menu": "^2.0.6",
17 | "@radix-ui/react-form": "^0.0.3",
18 | "@radix-ui/react-icons": "^1.3.0",
19 | "@radix-ui/react-label": "^2.0.2",
20 | "@radix-ui/react-slot": "^1.0.2",
21 | "@radix-ui/react-tabs": "^1.0.4",
22 | "@stripe/react-stripe-js": "^2.1.2",
23 | "@stripe/stripe-js": "^2.1.0",
24 | "@tanstack/react-table": "^8.10.7",
25 | "autoprefixer": "10.4.15",
26 | "axios": "^1.6.0",
27 | "class-variance-authority": "^0.7.0",
28 | "eslint": "8.47.0",
29 | "eslint-config-next": "13.4.19",
30 | "graphql": "^16.8.0",
31 | "jsonwebtoken": "^9.0.2",
32 | "lucide-react": "^0.287.0",
33 | "next": "^14.0.0",
34 | "postcss": "8.4.28",
35 | "react": "^18.2.0",
36 | "react-dom": "^18.2.0",
37 | "react-hook-form": "^7.47.0",
38 | "react-particles": "^2.12.2",
39 | "react-router-dom": "^6.15.0",
40 | "recharts": "^2.8.0",
41 | "sf-jwt-token": "^1.3.0",
42 | "stripe": "^13.8.0",
43 | "tailwindcss": "3.3.3",
44 | "tailwindcss-animate": "^1.0.7",
45 | "ts-node": "^10.9.1",
46 | "tsparticles": "^2.12.0",
47 | "zod": "^3.22.4"
48 | },
49 | "devDependencies": {
50 | "@types/jsonwebtoken": "^9.0.3",
51 | "@types/node": "^20.5.8",
52 | "@types/react": "^18.2.21",
53 | "@types/react-dom": "^18.2.7",
54 | "clsx": "^2.0.0",
55 | "eslint-config-prettier": "^9.0.0",
56 | "express": "^4.18.2",
57 | "prettier": "^3.0.3",
58 | "prettier-plugin-tailwindcss": "^0.5.4",
59 | "tailwind-merge": "^1.14.0",
60 | "typescript": "^5.2.2"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/bill-bot-baggins/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/bill-bot-baggins/public/ales-nesetril-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/ales-nesetril-background.jpg
--------------------------------------------------------------------------------
/bill-bot-baggins/public/archive/big-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/archive/big-logo.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/archive/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/archive/logo.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/bg-pattern.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/bg-pattern.jpeg
--------------------------------------------------------------------------------
/bill-bot-baggins/public/big-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/big-logo.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/dashboard-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/dashboard-2.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/github-mark-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/github-mark-white.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/invoice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/invoice.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/logo.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/splashpage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PayStream/873cc8533db29ff56061c8100f01476bf29394b4/bill-bot-baggins/public/splashpage.png
--------------------------------------------------------------------------------
/bill-bot-baggins/public/up-down.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/bill-bot-baggins/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/bill-bot-baggins/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 |
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "strict": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "Bundler",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "paths": {
27 | "@/*": [
28 | "./*"
29 | ]
30 | },
31 | "forceConsistentCasingInFileNames": true
32 | },
33 | "include": [
34 | "next-env.d.ts",
35 | "**/*.ts",
36 | "**/*.tsx",
37 | ".next/types/**/*.ts",
38 | "stripe-webhook/route.js"
39 | ],
40 | "exclude": [
41 | "node_modules"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ### Briefly describe your changes:
3 |
4 | **Jira issue number and link:**
5 |
6 | **Type of change:**
7 | - [ ] Bug fix (non-breaking change which fixes an issue)
8 | - [ ] New feature (non-breaking change which adds functionality)
9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
10 | - [ ] This change requires a documentation update
11 |
12 | ### Please answer the following:
13 |
14 | **1. What is the current behavior?**
15 | *If the feature did not exist, please say so.*
16 |
17 | **2. What is the new behavior?**
18 |
19 | **3. What tests did you run?**
20 |
21 | ### Checklist before requesting a review
22 | - [ ] I have performed a self-review of my code.
23 | - [ ] My code follows the style guidelines of this project
24 | - [ ] I have commented my code, particularly in hard-to-understand areas
25 | - [ ] I have added tests for my code.
26 | - [ ] I have resolved all merge conflicts.
--------------------------------------------------------------------------------
/salesforce-webhook/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.13
2 | WORKDIR /usr/src/app
3 | COPY * /usr/src/app/
4 | RUN npm install
5 | COPY . .
6 | # RUN npm start
7 | EXPOSE 3000
8 | CMD ["node", "./server.mjs"]
9 |
--------------------------------------------------------------------------------
/salesforce-webhook/eventHandler.js:
--------------------------------------------------------------------------------
1 | const { salesforceRouter } = require("./routers/salesforceRouter.js");
2 | const { stripeRouter } = require("./routers/stripeRouter.js");
3 |
4 | const { retreiveOppType, updateSalesforceStripeId, getPaymentType } =
5 | salesforceRouter;
6 |
7 | const { createStripeInvoice, payStripeInvoice, voidStripeInvoice } =
8 | stripeRouter;
9 |
10 | const recordTypes = [
11 | "SP",
12 | "BD",
13 | "FD",
14 | "LC",
15 | "RF",
16 | "OD",
17 | "FOC",
18 | "OA",
19 | "CA",
20 | "HR",
21 | "Customized",
22 | "SM",
23 | "CC",
24 | "EDLI",
25 | "DDP",
26 | ];
27 |
28 | const eventHandler = async (event) => {
29 | let opportunity;
30 | let paymentType = {};
31 | const { changeType, recordIds, changedFields } =
32 | event.payload.ChangeEventHeader;
33 | const { For_Chart__c, npe01__Payment_Amount__c, Name } = event.payload;
34 | const recordId = recordIds[0];
35 |
36 | if (changedFields.length === 1) return;
37 |
38 | switch (changeType) {
39 |
40 | case "CREATE": {
41 | console.log("CREATE case changeType: ", changeType);
42 | // initialize variable to payment record ID
43 | paymentType = For_Chart__c;
44 | // assign opp variable to the evaluated result of retrieveOppType function passing in recordId
45 | if (paymentType.string == "Cost to Client")
46 | opportunity = await retreiveOppType(recordId);
47 | else
48 | console.log(
49 | "This event does not meet the requirements for creatings a stripe invoice"
50 | );
51 | if (recordTypes.includes(opportunity.type)) {
52 | const paymentAmount = npe01__Payment_Amount__c;
53 | const invoice_number = Name;
54 | console.log("payment amount: ", paymentAmount.double);
55 |
56 | const paymentDetails = {
57 | account_name: opportunity.account_name,
58 | amount: paymentAmount.double,
59 | project_type: opportunity.project_type,
60 | invoice_number: invoice_number.string,
61 | recordId: recordId,
62 | };
63 | //create invoice in stripe
64 | const stripeInvoice = await createStripeInvoice(paymentDetails);
65 |
66 | //update salesforce record with stripe invoice id
67 | await updateSalesforceStripeId(recordId, stripeInvoice.id);
68 | }
69 | break;
70 | }
71 |
72 | case "UPDATE": {
73 |
74 | console.log("UPDATE case changeType: ", changeType);
75 |
76 | if (!For_Chart__c) {
77 | const clientPayment = getPaymentType(recordId);
78 | if (clientPayment) paymentType = "Cost to Client";
79 | }
80 | // console.log("UPDATE payment type: ", paymentType);
81 | const updates = {};
82 | changedFields.forEach((field) => {
83 | console.log("UPDATE change fields: ", changedFields);
84 | updates[field] = event.payload[field];
85 | });
86 | console.log("UPDATE updates object: ", updates);
87 | const { npe01__Paid__c, npe01__Written_Off__c, OutsideFundingSource__c } =
88 | updates;
89 |
90 | if (OutsideFundingSource__c) {
91 | console.log("OUTSIDE FUNDING SOURCE");
92 | }
93 | if (npe01__Written_Off__c) {
94 | console.log("MARK STRIPE INVOICE VOID");
95 | const written_off = npe01__Written_Off__c;
96 | written_off.boolean === true
97 | ? voidStripeInvoice(recordId)
98 | : console.log("invoice not marked void");
99 | }
100 | if (npe01__Paid__c) {
101 | const payobject = npe01__Paid__c;
102 | payobject.boolean === true
103 | ? payStripeInvoice(recordId)
104 | : console.log("invoice not marked paid");
105 | }
106 | break;
107 | }
108 | case "DELETE": {
109 | //need to store salesforce and stripe ids so that when /if a payment is deleted, it can get voided/deleted in stripe
110 | console.log("DELETE case");
111 | break;
112 | }
113 | default: {
114 | console.log("default case hit: ", JSON.stringify(event, null, 2));
115 | break;
116 | }
117 | }
118 | };
119 |
120 | module.exports = eventHandler;
121 |
--------------------------------------------------------------------------------
/salesforce-webhook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sf-pubsub-api",
3 | "version": "0.9.0",
4 | "main": "server.mjs",
5 | "scripts": {
6 | "start": "node server.mjs",
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "dependencies": {
10 | "axios": "^1.5.1",
11 | "express": "^4.17.2",
12 | "salesforce-pubsub-api-client": "^2.4.3",
13 | "sf-jwt-token": "1.3.0",
14 | "stripe": "^13.10.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/salesforce-webhook/routers/authRouter.js:
--------------------------------------------------------------------------------
1 | // import * as sfToken
2 | require("dotenv").config();
3 | const { getToken } = require("sf-jwt-token");
4 |
5 | const getSalesForceAccessToken = async () => {
6 | // convert the base64 private key into a string
7 | const {
8 | BASE64_PRIVATE_KEY,
9 | SALESFORCE_CLIENT_ID,
10 | SALESFORCE_USERNAME,
11 | TEST_URL,
12 | } = process.env;
13 |
14 | const privateKey = Buffer.from(BASE64_PRIVATE_KEY, "base64").toString(
15 | "utf-8"
16 | );
17 |
18 | try {
19 | // gets a new (if it hasn't expired it will send the still active token) access token from sales force
20 | const jwtTokenResponse = await getToken({
21 | iss: SALESFORCE_CLIENT_ID,
22 | sub: SALESFORCE_USERNAME,
23 | aud: TEST_URL,
24 | privateKey: privateKey,
25 | });
26 |
27 | return jwtTokenResponse.access_token;
28 | } catch (error) {
29 | if (error instanceof Error) {
30 | console.error("Error fetching token:", error);
31 | }
32 | throw Error;
33 | }
34 | };
35 |
36 | module.exports = { getSalesForceAccessToken };
37 |
--------------------------------------------------------------------------------
/salesforce-webhook/routers/salesforceRouter.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const { getSalesForceAccessToken } = require("./authRouter.js");
3 |
4 | const salesforceRouter = {};
5 |
6 | salesforceRouter.getStripeId = async (recordId) => {
7 | const access_token = await getSalesForceAccessToken();
8 | const data = JSON.stringify({
9 | query: `query payments ($Id: ID) {
10 | uiapi {
11 | query {
12 | npe01__OppPayment__c (where: { Id: { eq: $Id } }) {
13 | edges {
14 | node {
15 | Stripe_Invoice_ID__c {
16 | value
17 | }
18 | }
19 | }
20 | }
21 | }
22 | }
23 | }`,
24 | variables: { Id: recordId },
25 | });
26 |
27 | const config = {
28 | method: "post",
29 | maxBodyLength: Infinity,
30 | url: process.env.SALESFORCE_GRAPHQL_URI,
31 | headers: {
32 | "X-Chatter-Entity-Encoding": "false",
33 | "Content-Type": "application/json",
34 | Authorization: `Bearer ${access_token}`,
35 | Cookie: process.env.SALESFORCE_COOKIE_AUTH,
36 | },
37 | data: data,
38 | };
39 | const fetchStripeId = await axios.request(config);
40 | const stripeId =
41 | fetchStripeId.data.data.uiapi.query.npe01__OppPayment__c.edges[0].node
42 | .Stripe_Invoice_ID__c.value;
43 | return stripeId;
44 | };
45 |
46 | salesforceRouter.getPaymentType = async (id) => {
47 | const access_token = await getSalesForceAccessToken();
48 | let clientPayment = false;
49 | const data = JSON.stringify({
50 | query: `query payments ($Id: ID) {
51 | uiapi {
52 | query {
53 | npe01__OppPayment__c (where: { Id: { eq: $Id } }) {
54 | edges {
55 | node {
56 | For_Chart__c {
57 | value
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }`,
65 | variables: { Id: id },
66 | });
67 | const config = {
68 | method: "post",
69 | maxBodyLength: Infinity,
70 | url: process.env.SALESFORCE_GRAPHQL_URI,
71 | headers: {
72 | "X-Chatter-Entity-Encoding": "false",
73 | "Content-Type": "application/json",
74 | Authorization: `Bearer ${access_token}`,
75 | Cookie: process.env.SALESFORCE_COOKIE_AUTH,
76 | },
77 | data: data,
78 | };
79 |
80 | const fetchPaymentType = await axios.request(config);
81 | const paymentType =
82 | fetchPaymentType.data.data.uiapi.query.npe01__OppPayment__c.edges[0].node
83 | .For_Chart__c.value;
84 | if (paymentType === "Cost to Client") clientPayment = true;
85 | return clientPayment;
86 | };
87 |
88 | /**
89 | * helper function - graphQL api call to salesforce for opportunity record ID
90 | * @param string - payment record id from data change capture event
91 | * @return string - opportunity record id
92 | */
93 | salesforceRouter.getOppRecordId = async (id) => {
94 | const access_token = await getSalesForceAccessToken();
95 | const data = JSON.stringify({
96 | query: `query payments ($Id: ID) {
97 | uiapi {
98 | query {
99 | npe01__OppPayment__c (where: { Id: { eq: $Id } }) {
100 | edges {
101 | node {
102 | Id
103 | Opportunity_Account_Name__c {
104 | value
105 | }
106 | npe01__Opportunity__c {
107 | value
108 | }
109 | }
110 | }
111 | }
112 | }
113 | }
114 | }`,
115 | variables: { Id: id },
116 | });
117 | const config = {
118 | method: "post",
119 | maxBodyLength: Infinity,
120 | url: process.env.SALESFORCE_GRAPHQL_URI,
121 | headers: {
122 | "X-Chatter-Entity-Encoding": "false",
123 | "Content-Type": "application/json",
124 | Authorization: `Bearer ${access_token}`,
125 | Cookie: process.env.SALESFORCE_COOKIE_AUTH,
126 | },
127 | data: data,
128 | };
129 |
130 | const fetchOppId = await axios.request(config);
131 | const oppId =
132 | fetchOppId.data.data.uiapi.query.npe01__OppPayment__c.edges[0].node
133 | .npe01__Opportunity__c.value;
134 |
135 | return oppId;
136 | };
137 |
138 | /**
139 | * graphQL salesforce API call to retrive opportunity record type for payment record captured in data change event
140 | * @param {*} id
141 | * @returns object with opportunty type abbreviation, type - long form, and account name properties
142 | */
143 |
144 | salesforceRouter.retreiveOppType = async (id) => {
145 | const access_token = await getSalesForceAccessToken();
146 | const fetchOppId = await salesforceRouter.getOppRecordId(id);
147 |
148 | const data = JSON.stringify({
149 | query: `query payments ($Id: ID) {
150 | uiapi {
151 | query {
152 | Opportunity (where: { Id: { eq: $Id } }) {
153 | edges {
154 | node {
155 | Id
156 | Name {
157 | value
158 | }
159 | Type {
160 | value
161 | }
162 | FOR_CONGA_Engagement_Type__c {
163 | value
164 | }
165 | Account {
166 | Name {
167 | value
168 | }
169 | }
170 | }
171 | }
172 | }
173 | }
174 | }
175 | }`,
176 | variables: { Id: fetchOppId },
177 | });
178 | const config = {
179 | method: "post",
180 | maxBodyLength: Infinity,
181 | url: process.env.SALESFORCE_GRAPHQL_URI,
182 | headers: {
183 | "X-Chatter-Entity-Encoding": "false",
184 | "Content-Type": "application/json",
185 | Authorization: `Bearer ${access_token}`,
186 | Cookie: process.env.SALESFORCE_COOKIE_AUTH,
187 | },
188 | data: data,
189 | };
190 | const fetchOppType = await axios.request(config);
191 | const {
192 | Id,
193 | Type,
194 | npsp__Primary_Contact__c,
195 | FOR_CONGA_Engagement_Type__c,
196 | Account,
197 | } = fetchOppType.data.data.uiapi.query.Opportunity.edges[0].node;
198 | const opportunity = {
199 | type: Type.value,
200 | project_type: FOR_CONGA_Engagement_Type__c.value,
201 | // primary_contact_id: npsp__Primary_Contact__c.value,
202 | account_name: Account.Name.value,
203 | sf_opp_id: Id,
204 | };
205 | return opportunity;
206 | };
207 |
208 | salesforceRouter.updateSalesforceStripeId = async (
209 | recordId,
210 | stripeinvoiceId
211 | ) => {
212 | const access_token = await getSalesForceAccessToken();
213 | const data = JSON.stringify({
214 | allowSaveOnDuplicate: false,
215 | fields: {
216 | Stripe_Invoice_ID__c: stripeinvoiceId,
217 | },
218 | });
219 |
220 | const config = {
221 | method: "patch",
222 | maxBodyLength: Infinity,
223 | url: `${process.env.SALESFORCE_API_URI}/${recordId}`,
224 | headers: {
225 | "Content-Type": "application/json",
226 | Authorization: `Bearer ${access_token}`,
227 | Cookie: process.env.SALESFORCE_COOKIE_AUTH,
228 | },
229 | data: data,
230 | };
231 |
232 | axios
233 | .request(config)
234 | .then((response) => {
235 | console.log(JSON.stringify(response.data));
236 | })
237 | .catch((error) => {
238 | console.log(error);
239 | });
240 | };
241 |
242 | exports.salesforceRouter = salesforceRouter;
243 |
--------------------------------------------------------------------------------
/salesforce-webhook/routers/stripeRouter.js:
--------------------------------------------------------------------------------
1 | const Stripe = require("stripe");
2 | const { salesforceRouter } = require("./salesforceRouter.js");
3 |
4 | const { getStripeId } = salesforceRouter;
5 |
6 | const config = {
7 | apiVersion: "2023-08-16",
8 | typescript: true,
9 | };
10 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, config);
11 |
12 | const stripeRouter = {};
13 | /**
14 | * creates invoice in Stripe to match payment record received from salesforce
15 | * @param Object - data needed to create stripe invoice
16 | * @return stripe invoice ID
17 | */
18 | stripeRouter.createStripeInvoice = async (paymentInfo) => {
19 | /**
20 | * retrieve customer list from stripe
21 | */
22 | const customerList = await stripe.customers.list();
23 | let customerId;
24 | customerList.data.forEach((element) => {
25 | if (element.name === paymentInfo.account_name) {
26 | customerId = element.id;
27 | return;
28 | }
29 | });
30 |
31 | /**
32 | * if account is not in stripe yet, create account and retrieve the customer id to send the invoice to
33 | * all invoices will be created with an ESCSC.org billing email for now - wil not send emails to clients from Stripe
34 | */
35 | if (!customerId) {
36 | // Create a new Customer
37 | console.log("payment deets.account_name ", paymentInfo.account_name);
38 | const customer = await stripe.customers.create({
39 | name: paymentInfo.account_name,
40 | email: "lcharity@escsc.org",
41 | });
42 |
43 | customerId = customer.id;
44 | }
45 |
46 | /**
47 | * create new invoice in stripe based on invoice information from salesforce (on paymentInfo arg)
48 | */
49 | const newInvoice = await stripe.invoices.create({
50 | customer: customerId,
51 | auto_advance: false,
52 | collection_method: "send_invoice",
53 | days_until_due: 30,
54 | });
55 |
56 | console.log("new stripe invoice created in webhook route: ", newInvoice);
57 |
58 | /**
59 | * need project type from salesforce (passed in on arg object) to create the "product type" and then assign a default price (also on passed in object)
60 | */
61 | const product = await stripe.products.create({
62 | name: `${paymentInfo.project_type} Project Invoice #: ${paymentInfo.invoice_number} `,
63 | default_price_data: {
64 | currency: "usd",
65 | unit_amount: paymentInfo.amount * 100,
66 | },
67 | });
68 |
69 | /**
70 | * add line item to invoice just created
71 | */
72 | await stripe.invoiceItems.create({
73 | customer: customerId,
74 | price: product.default_price,
75 | invoice: newInvoice.id,
76 | });
77 |
78 | /**
79 | * retrieve final invoice from stripe
80 | * */
81 | const finalInvoice = await stripe.invoices.finalizeInvoice(newInvoice.id);
82 | return finalInvoice;
83 | };
84 |
85 | /** look up invoice in stripe to see if exists */
86 |
87 | /**update invoice in stripe */
88 | stripeRouter.payStripeInvoice = async (recordId, stripeInvoiceDetails) => {
89 | try {
90 | const stripeInvoiceId = await getStripeId(recordId);
91 | if (await stripe.invoices.retrieve(stripeInvoiceId)) {
92 | const updatedInvoice = await stripe.invoices.pay(stripeInvoiceId, {
93 | paid_out_of_band: true,
94 | });
95 | return updatedInvoice;
96 | }
97 | } catch (error) {
98 | console.log(error);
99 | }
100 | };
101 |
102 | stripeRouter.voidStripeInvoice = async (recordId) => {
103 | try {
104 | const stripeInvoiceId = await getStripeId(recordId);
105 | if (await stripe.invoices.retrieve(stripeInvoiceId)) {
106 | const updatedInvoice = await stripe.invoices.voidInvoice(stripeInvoiceId);
107 | return updatedInvoice;
108 | }
109 | } catch (error) {
110 | console.log(error);
111 | }
112 | };
113 |
114 | exports.stripeRouter = stripeRouter;
115 |
--------------------------------------------------------------------------------
/salesforce-webhook/salesforce-pub-sub-api.mjs:
--------------------------------------------------------------------------------
1 | import PubSubApiClient from "salesforce-pubsub-api-client";
2 | import eventHandler from "./eventHandler.js";
3 | import dotenv from "dotenv";
4 | dotenv.config();
5 | //destructure assignemtn for env variables
6 | const {
7 | SALESFORCE_LOGIN_URL,
8 | SALESFORCE_TOKEN,
9 | SALESFORCE_USERNAME,
10 | SALESFORCE_ORG_ID,
11 | } = process.env;
12 |
13 | import { getSalesForceAccessToken } from "./routers/authRouter.js";
14 |
15 | /**
16 | * PubSub API - webhook for listening to Salesforce Payment record changes adn handling events based on their type
17 | */
18 | const salesforceController = async () => {
19 | try {
20 | const access_token = await getSalesForceAccessToken();
21 | const client = new PubSubApiClient();
22 | // console.log("instanceUrl: ", SALESFORCE_LOGIN_URL);
23 | //create new connection
24 |
25 | //open new connection
26 | await client.connectWithAuth(
27 | `Bearer ${access_token}`,
28 | SALESFORCE_LOGIN_URL,
29 | SALESFORCE_ORG_ID,
30 | SALESFORCE_USERNAME
31 | );
32 |
33 | //subscribe to data change events
34 | const eventEmitter = await client.subscribe(
35 | "/data/npe01__OppPayment__ChangeEvent",
36 | 25
37 | );
38 |
39 | // Handle incoming events
40 | eventEmitter.on("data", async (event) => {
41 | console.log(
42 | `Handling ${event.payload.ChangeEventHeader.entityName} change event ${event.replayId}`
43 | );
44 | eventHandler(event);
45 | });
46 | } catch (error) {
47 | console.error(error);
48 | // if metadata/auth error - submit refresh token for new access token
49 | }
50 | };
51 |
52 | // salesforceController.run();
53 | export default salesforceController;
54 |
--------------------------------------------------------------------------------
/salesforce-webhook/server.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 |
3 | import salesforceController from "./salesforce-pub-sub-api.mjs";
4 |
5 | const app = express();
6 | app.use(express.json());
7 |
8 | /**
9 | * open salesforce pub-sub API connection
10 | */
11 | // await getToken();
12 | salesforceController();
13 | // app.use()
14 |
15 | app.listen(3030, () => console.log("server is listening on port 3030"));
16 |
--------------------------------------------------------------------------------
/stripe-webhook/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.13
2 | WORKDIR /usr/src/app
3 | COPY * /usr/src/app/
4 | RUN npm install
5 | COPY . .
6 | # RUN npm start
7 | EXPOSE 3000
8 | CMD ["node", "./route.js"]
9 |
--------------------------------------------------------------------------------
/stripe-webhook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stripe-webhook-api",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "type": "module",
6 | "scripts": {
7 | "start": "node route.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "dependencies": {
11 | "axios": "^1.5.1",
12 | "express": "^4.18.2",
13 | "stripe": "^13.8.0",
14 | "sf-jwt-token": "1.3.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/stripe-webhook/route.js:
--------------------------------------------------------------------------------
1 |
2 | import dotenv from "dotenv"
3 | import {salesforcePaid} from "./stripe_salesforce_fetch.js";
4 | import Stripe from "stripe";
5 | import express from "express";
6 | import bodyparser from "body-parser"
7 |
8 | //start stripe session with stripe specific key
9 |
10 | dotenv.config()
11 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
12 | const app = express();
13 |
14 |
15 | // create the route that is going to be hit by Stripe when an event takes place
16 | // this will require stripe login and stripe listen --forward-to localhost:4242/webhook into the CLI
17 | // will be temporary until we get our application on a public server
18 |
19 | app.post('/webhook', bodyparser.raw({ type: 'application/json' }), async (request, response) => {
20 | const payload = request.body;
21 | const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET
22 | const sig = request.headers['stripe-signature'];
23 | let event;
24 |
25 | try {
26 | event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
27 | console.log(event.type)
28 | } catch (err) {
29 | response.status(400).send(`Webhook Error: ${err.message}`);
30 | return;
31 | }
32 |
33 | // Handle the event
34 | switch (event.type) {
35 | case 'invoice.paid':
36 | const invoicePaid = event.data.object;
37 | // this is where we would tap into the Salesforce webhook to update an invoice as paid (logic made just need to tie it in)
38 | salesforcePaid(invoicePaid.id)
39 |
40 | break;
41 | // ... handle other event types
42 | default:
43 | console.log(`Unhandled event type ${event.type}`);
44 | }
45 |
46 | // Return a 200 response to acknowledge receipt of the event
47 | response.send();
48 | }
49 | );
50 |
51 | app.listen(4242, () => console.log('Running on port 4242'));
52 |
--------------------------------------------------------------------------------
/stripe-webhook/stripe_salesforce_fetch.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dotenv from 'dotenv';
3 |
4 | dotenv.config();
5 |
6 | const createConfig = (method, url, data) => {
7 | return {
8 | method,
9 | maxBodyLength: Infinity,
10 | url,
11 | headers: {
12 | 'X-Chatter-Entity-Encoding': 'false',
13 | 'Content-Type': 'application/json',
14 | 'Authorization': process.env.SALESFORCE_TOKEN,
15 | 'Cookie': process.env.SALESFORCE_COOKIE_AUTH
16 | },
17 | data
18 | }
19 | }
20 |
21 | export const salesforcePaid = async (stripeId) => {
22 |
23 | // query to fetch the salesforce record id based on the stripeId
24 |
25 | let fetchData = JSON.stringify({
26 | query: `query payments ($stripeId: TextArea) {
27 | uiapi {
28 | query {
29 | npe01__OppPayment__c (where: { Stripe_Invoice_ID__c: { eq: $stripeId } }) {
30 | edges {
31 | node {
32 | Id
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }`,
39 | variables: {"stripeId":stripeId}
40 | });
41 |
42 | let fetchConfig = createConfig('post', `https://escsocal--lc001.sandbox.my.salesforce.com/services/data/v58.0/graphql`, fetchData)
43 |
44 | const fetchedRecordData = await axios.request(fetchConfig)
45 |
46 | // take the data back from the query and access the record id for the respective salesforce record id
47 | const recordId = fetchedRecordData.data.data.uiapi.query.npe01__OppPayment__c.edges[0].node.Id;
48 |
49 | // get the current date in a format acceptable by salesforce
50 | const currentDate = (new Date(Date.now() - (new Date()).getTimezoneOffset()*60000)).toISOString().split('T')[0]
51 |
52 | // use that record id to mark the invoice at that record id as paid in salesforce and include the current date paid
53 | let payData = JSON.stringify({
54 | "allowSaveOnDuplicate": false,
55 | "fields": {
56 | "npe01__Paid__c": true,
57 | "npe01__Payment_Method__c": "Credit Card",
58 | "npe01__Payment_Date__c": currentDate
59 | }
60 | });
61 |
62 | let config = createConfig('patch', `https://escsocal--lc001.sandbox.my.salesforce.com/services/data/v58.0/ui-api/records/${recordId}`, payData)
63 | const resultant = await axios.request(config);
64 |
65 | }
66 |
--------------------------------------------------------------------------------