Skip to content

Instantly share code, notes, and snippets.

@50-Course
Created August 11, 2025 16:50
Show Gist options
  • Save 50-Course/c9e8345f313ff39a2003790d133503fe to your computer and use it in GitHub Desktop.
Save 50-Course/c9e8345f313ff39a2003790d133503fe to your computer and use it in GitHub Desktop.
export class AutomationService {
private readonly logger = new Logger(AutomationService.name);
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(CardLog)
private cardLogRepository: Repository<CardLog>,
) {}
async addCardToParamount(
input: CardAutomationInput,
): Promise<AutomationResponse> {
let browser;
let page;
try {
const user = await this.userRepository.findOne({
where: { id: input.userId },
});
if (!user) {
throw new NotFoundException("User not found");
}
if (!user.paramountEmail || !user.paramountPassword) {
throw new Error("User does not have Paramount+ credentials");
}
this.logger.log(`Starting card automation for user: ${user.email}`);
browser = await puppeteer.launch({
headless: false,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
page = await browser.newPage();
await page.setViewport({ width: 1366, height: 768 });
// here we choose to navigate to Paramount+ sign-in page directly because we know the URL
await this.retryAction(async () => {
await page.goto("https://www.paramountplus.com/account/signin/", {
waitUntil: "networkidle2",
});
}, "SIGN IN");
await this.retryAction(async () => {
await page.waitForSelector('input[type="email"]', { timeout: 50000 });
await page.type('input[type="email"]', user.paramountEmail);
await page.type('input[type="password"]', user.paramountPassword);
await page.click("button.qt-continuebtn");
// at signup, if vpn is not on, or location is not US, we hit some errors, we listen to that error
// and rethrow it,
//
// we could otherwise, display a structured format of this error in our API layer (the graphQL response as an ErrorResponse)
try {
const errorSelector = "p.form-message.error";
await page.waitForSelector(errorSelector, { timeout: 50000 });
const errorElement = await page.$(errorSelector);
const errorMessage = await page.evaluate(
(el) => el.textContent.trim(),
errorElement,
);
throw new Error(`Login failed: ${errorMessage}`);
} catch (e) {
if (e.name !== "TimeoutError") {
throw e;
}
}
}, "Login");
// upon login, we're redirected to a paywalled page, and attempt to select a payment plan from the payment options plans
await this.retryAction(async () => {
await page.waitForNavigation({ waitUntil: "networkidle2" });
}, "Wait for login and redirect to plan selection");
// as a decision, we select the cheaper payment option plan, our users' can decide to upgrade in future
await this.retryAction(async () => {
await page.waitForSelector("button.qt-continuebtn:nth-of-type(2)", {
timeout: 10000,
});
await page.click("button.qt-continuebtn:nth-of-type(2)");
}, "Select the cheaper plan option");
await this.retryAction(async () => {
await page.waitForNavigation({ waitUntil: "networkidle2" });
}, "Wait for redirect to payment page");
// finally, we would fill in the user's payment credentials, submit and log thi payment entry
await this.retryAction(async () => {
await page.waitForSelector(
'input[name*="card"], input[placeholder*="card"]',
{ timeout: 10000 },
);
await page.type(
'input[name*="card"], input[placeholder*="card"]',
input.cardNumber,
);
await page.type(
'input[name*="expiry"], input[placeholder*="MM"]',
input.expiryMonth,
);
await page.type(
'input[name*="expiry"], input[placeholder*="YY"]',
input.expiryYear,
);
await page.type(
'input[name*="cvv"], input[placeholder*="CVV"]',
input.cvv,
);
if (input.cardholderName) {
await page.type(
'input[name*="name"], input[placeholder*="name"]',
input.cardholderName,
);
}
await page.click(
'button[type="submit"], button:contains("Save"), button:contains("Add")',
);
}, "Fill card details");
await this.retryAction(async () => {
await page.waitForSelector(
'.success, .alert-success, [data-testid="success"]',
{ timeout: 15000 },
);
}, "Wait for success confirmation");
await this.logCardOperation(
input.userId,
"ADD_CARD",
"SUCCESS",
undefined,
input.cardNumber.slice(-4),
);
this.logger.log(
`Card automation completed successfully for user: ${user.email}`,
);
return {
success: true,
message: "Card added successfully to Paramount+",
};
} catch (error) {
// at this point, we can assume our operation is failing, we:
// log the error for tracing, and debugging
//
// and return a structured response back to our user
this.logger.error(
`Card automation failed: ${error.message}`,
error.stack,
);
await this.logCardOperation(
input.userId,
"ADD_CARD",
"FAILED",
error.message,
input.cardNumber.slice(-4),
);
return {
success: false,
errorMessage: error.message,
};
} finally {
if (page) await page.close();
if (browser) await browser.close();
}
}
// leave every other stuffs the same
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment