Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/config/quickbooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// src/config/quickbooks.js
require('dotenv').config();

module.exports = {
clientId: process.env.QB_CLIENT_ID,
clientSecret: process.env.QB_CLIENT_SECRET,
// 'sandbox' for dev/testing, switch to 'production' when you go live
environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
// Must exactly match what you configured in the Intuit Developer Portal
redirectUri: process.env.QB_REDIRECT_URI,
scopes: [
'com.intuit.quickbooks.accounting',
'com.intuit.quickbooks.payment'
// add 'openid', 'profile', etc., here only if you need them
]
};
10 changes: 10 additions & 0 deletions src/features/courses/course.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const { createCourse, getCourseById, updateCourse, deleteCourse } = require('./course.service');

router.post('/', createCourse);
router.get('/:id', getCourseById);
router.put('/:id', updateCourse);
router.delete('/:id', deleteCourse);

module.exports = router;
Empty file.
91 changes: 91 additions & 0 deletions src/features/courses/course.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const supabase = require('../../config/supabase');


const createCourse = async (req, res, next) => {
const { title, price, description, video } = req.body;

if (!title || price === undefined || !description || !video) {
return res.status(400).json({ error: 'All fields are required' });
}

try {
const { error } = await supabase
.from('courses')
.insert([{ title, price, description, video }]);

if (error) throw error;

return res.status(200).end();
} catch (err) {
next(err);
}
};


// GET /courses/:id
const getCourseById = async (req, res, next) => {
const { id } = req.params;

try {
const { data, error } = await supabase
.from('courses')
.select('title, price, description, video')
.eq('id', id)
.single();

if (error || !data) {
return res.status(400).json({ error: 'Course not found' });
}

res.status(200).json(data);
} catch (err) {
next(err);
}
};


// PUT /courses/:id
const updateCourse = async (req, res, next) => {
const { id } = req.params;
const { title, price, description, video } = req.body;

if (!title || price === undefined || !description || !video) {
return res.status(400).json({ error: 'All fields are required' });
}

try {
const { error } = await supabase
.from('courses')
.update({ title, price, description, video })
.eq('id', id);

if (error) throw error;

res.status(200).end();
} catch (err) {
next(err);
}
};


// DELETE /courses/:id
const deleteCourse = async (req, res, next) => {
const { id } = req.params;

try {
const { error } = await supabase.from('courses').delete().eq('id', id);

if (error) throw error;

res.status(200).end();
} catch (err) {
next(err);
}
};

module.exports = {
createCourse,
getCourseById,
updateCourse,
deleteCourse,
};
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
t
73 changes: 73 additions & 0 deletions src/features/quickbooks/services/quickbooksAuthService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// src/features/quickbooks/services/quickbooksAuthService.js
const QuickBooks = require('node-quickbooks');
const {
clientId,
clientSecret,
environment,
redirectUri,
scopes
} = require('../../../config/quickbooks');
const { loadTokens, saveTokens } = require('../utils/tokenUtils');

// Initialize the Intuit OAuth client
const oauthClient = new QuickBooks.OAuthClient({
clientId,
clientSecret,
environment, // 'sandbox' or 'production'
redirectUri // must match your env var and QuickBooks app settings
});

/**
* Generate the URL that the merchant must visit to consent to your app.
* @param {string} state A CSRF token or random string to validate the callback
* @returns {string} Full OAuth2 consent URL
*/
function generateConsentUrl(state) {
return oauthClient.authorizeUri({
scope: scopes, // e.g. ['com.intuit.quickbooks.accounting','com.intuit.quickbooks.payment']
state
});
}

/**
* After QuickBooks redirects back to your callback endpoint, exchange
* the full callback URL (with code & realmId) for access & refresh tokens,
* then persist them.
*
* @param {string} callbackUrl full req.url from Express (including ?code=…&realmId=…)
* @param {number} merchantId your internal ID for this merchant
* @returns {Promise<{
* realmId: string,
* accessToken: string,
* refreshToken: string,
* expiresAt: string
* }>}
*/
async function handleAuthCallback(callbackUrl, merchantId) {
// Exchange the authorization code for tokens
const authResponse = await oauthClient.createToken(callbackUrl);

// Extract the JSON and relevant fields
const tokenJson = authResponse.getJson();
const { access_token, refresh_token, expires_in } = tokenJson;
const realmId = authResponse.token.realmId;

// Calculate an absolute expiry timestamp
const expiresAt = new Date(Date.now() + expires_in * 1000).toISOString();

// Persist into Supabase via our tokenUtils
await saveTokens({
merchantId,
realmId,
accessToken: access_token,
refreshToken: refresh_token,
expiresAt
});

return { realmId, accessToken: access_token, refreshToken: refresh_token, expiresAt };
}

module.exports = {
generateConsentUrl,
handleAuthCallback
};
Empty file.
71 changes: 71 additions & 0 deletions src/features/quickbooks/utils/tokenUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// src/features/quickbooks/utils/tokenUtils.js
// Helpers for loading and saving QuickBooks tokens using Supabase

// Import your initialized Supabase client
const supabase = require('../../../../config/supabase');

/**
* Fetch stored QuickBooks tokens for a given merchant.
* @param {number} merchantId
* @returns {Promise<{
* merchantId: number,
* realmId: string,
* accessToken: string,
* refreshToken: string,
* expiresAt: string
* }|null>}
*/
async function loadTokens(merchantId) {
const { data, error } = await supabase
.from('quickbooks_tokens')
.select('merchant_id, realm_id, access_token, refresh_token, expires_at')
.eq('merchant_id', merchantId)
.single();

if (error) {
// If no row found, return null; otherwise, propagate error
if (error.code === 'PGRST116') return null;
throw error;
}

return {
merchantId: data.merchant_id,
realmId: data.realm_id,
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: data.expires_at
};
}

/**
* Insert or update QuickBooks tokens for a merchant.
* Relies on a unique constraint on `merchant_id` for upsert.
* @param {Object} params
* @param {number} params.merchantId
* @param {string} params.realmId
* @param {string} params.accessToken
* @param {string} params.refreshToken
* @param {Date|string} params.expiresAt
*/
async function saveTokens({ merchantId, realmId, accessToken, refreshToken, expiresAt }) {
const { error } = await supabase
.from('quickbooks_tokens')
.upsert(
{
merchant_id: merchantId,
realm_id: realmId,
access_token: accessToken,
refresh_token: refreshToken,
expires_at: expiresAt,
updated_at: new Date().toISOString()
},
{ onConflict: 'merchant_id' }
);

if (error) throw error;
}

module.exports = {
loadTokens,
saveTokens
};
19 changes: 19 additions & 0 deletions testConnection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config();

const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);

async function testDB() {
const { data, error } = await supabase.from('courses').select('*').limit(1);

if (error) {
console.error('❌ Connection Failed:', error.message);
} else {
console.log('✅ Connection Successful! Sample data:', data);
}
}

testDB();