Portfolio update emails (#928)
* Stats computing correctly * Styles propagating - testing in prod now * Formatting html * Reset portfolio flag on mondays at 12am * Add profit, styling * More styling, less reports * Cleanup * Comments * comment * Try to send higher signal emails * Send emails to proper email address
This commit is contained in:
parent
2ef025a151
commit
df316fc4da
|
@ -21,6 +21,25 @@ const computeInvestmentValue = (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const computeInvestmentValueCustomProb = (
|
||||||
|
bets: Bet[],
|
||||||
|
contract: Contract,
|
||||||
|
p: number
|
||||||
|
) => {
|
||||||
|
return sumBy(bets, (bet) => {
|
||||||
|
if (!contract || contract.isResolved) return 0
|
||||||
|
if (bet.sale || bet.isSold) return 0
|
||||||
|
const { outcome, shares } = bet
|
||||||
|
|
||||||
|
const betP = outcome === 'YES' ? p : 1 - p
|
||||||
|
|
||||||
|
const payout = betP * shares
|
||||||
|
const value = payout - (bet.loanAmount ?? 0)
|
||||||
|
if (isNaN(value)) return 0
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
|
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
|
||||||
const periodFilteredContracts = userContracts.filter(
|
const periodFilteredContracts = userContracts.filter(
|
||||||
(contract) => contract.createdTime >= startTime
|
(contract) => contract.createdTime >= startTime
|
||||||
|
|
|
@ -57,6 +57,7 @@ export type PrivateUser = {
|
||||||
|
|
||||||
email?: string
|
email?: string
|
||||||
weeklyTrendingEmailSent?: boolean
|
weeklyTrendingEmailSent?: boolean
|
||||||
|
weeklyPortfolioUpdateEmailSent?: boolean
|
||||||
manaBonusEmailSent?: boolean
|
manaBonusEmailSent?: boolean
|
||||||
initialDeviceToken?: string
|
initialDeviceToken?: string
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
|
|
|
@ -0,0 +1,411 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Weekly Portfolio Update on Manifold</title>
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
font-family:"Readex Pro", Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
table { margin: 0 auto; }
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0;
|
||||||
|
mso-table-rspace: 0;
|
||||||
|
}
|
||||||
|
th {color:#000000; font-size:17px;}
|
||||||
|
th, td {padding: 10px; }
|
||||||
|
td{ font-size: 17px}
|
||||||
|
th, td { vertical-align: center; text-align: left }
|
||||||
|
a { vertical-align: center; text-align: left}
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
p.change{
|
||||||
|
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
|
||||||
|
}
|
||||||
|
p.prob{
|
||||||
|
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
|
||||||
|
}
|
||||||
|
a.question{
|
||||||
|
font-size: 18px;display: inline; vertical-align: middle;
|
||||||
|
}
|
||||||
|
td.question{
|
||||||
|
vertical-align: middle; padding-bottom: 15px; text-align: left;
|
||||||
|
}
|
||||||
|
td.probs{
|
||||||
|
text-align: right; padding-left: 10px; min-width: 115px
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<noscript>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG />
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
</noscript>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||||
|
<style type="text/css">
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||||
|
</style>
|
||||||
|
<!--<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width: 480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
[owa] .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
table.mj-full-width-mobile {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mj-full-width-mobile {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="word-spacing: normal; background-color: #f4f4f4">
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="border-collapse:collapse;border-spacing:0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:550px;">
|
||||||
|
|
||||||
|
<a href="https://manifold.markets" target="_blank">
|
||||||
|
|
||||||
|
<img alt="banner logo" height="auto"
|
||||||
|
src="https://manifold.markets/logo-banner.png"
|
||||||
|
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||||
|
title="" width="550">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="
|
||||||
|
background: #ffffff;
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin: 0px auto;
|
||||||
|
max-width: 600px;
|
||||||
|
">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="background: #ffffff; background-color: #ffffff; width: 100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 20px 0px 0px 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="vertical-align: top; margin-bottom: 30px" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
|
data-testid="4XoHRGw1Y"><span
|
||||||
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
</span>Hi {{name}},</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 0px;"
|
||||||
|
data-testid="4XoHRGw1Y">
|
||||||
|
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
We ran the numbers and here's how you did this past week!
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
|
||||||
|
<tr>
|
||||||
|
<tr>
|
||||||
|
<th style='font-size: 22px; text-align: center'>
|
||||||
|
Profit
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style='padding-bottom: 30px; text-align: center'>
|
||||||
|
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
|
||||||
|
{{profit}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<td align="center"
|
||||||
|
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="border-collapse:collapse;border-spacing:0px; ">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style='width: 170px'>
|
||||||
|
🔥 Prediction streak
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{prediction_streak}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
💸 Tips received
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{tips_received}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
📈 Markets traded
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{markets_traded}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
❓ Markets created
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{{markets_created}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style='width: 55px'>
|
||||||
|
🥳 Traders attracted
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{unique_bettors}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 0 0 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="width: 100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 20px 0px 20px 0px;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align: top; padding: 0">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0"
|
||||||
|
role="presentation" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
font-family: Ubuntu, Helvetica, Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
color: #000000;
|
||||||
|
">
|
||||||
|
<p style="margin: 10px 0">
|
||||||
|
This e-mail has been sent to
|
||||||
|
{{name}},
|
||||||
|
<a href="{{unsubscribeUrl}}" style="
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
510
functions/src/email-templates/weekly-portfolio-update.html
Normal file
510
functions/src/email-templates/weekly-portfolio-update.html
Normal file
|
@ -0,0 +1,510 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Weekly Portfolio Update on Manifold</title>
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
font-family:"Readex Pro", Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
table { margin: 0 auto; }
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0;
|
||||||
|
mso-table-rspace: 0;
|
||||||
|
}
|
||||||
|
th {color:#000000; font-size:17px;}
|
||||||
|
th, td {padding: 10px; }
|
||||||
|
td{ font-size: 17px}
|
||||||
|
th, td { vertical-align: center; text-align: left }
|
||||||
|
a { vertical-align: center; text-align: left}
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
p.change{
|
||||||
|
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
|
||||||
|
}
|
||||||
|
p.prob{
|
||||||
|
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
|
||||||
|
}
|
||||||
|
a.question{
|
||||||
|
font-size: 18px;display: inline; vertical-align: middle;
|
||||||
|
}
|
||||||
|
td.question{
|
||||||
|
vertical-align: middle; padding-bottom: 15px; text-align: left;
|
||||||
|
}
|
||||||
|
td.probs{
|
||||||
|
text-align: right; padding-left: 10px; min-width: 115px
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<noscript>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG />
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
</noscript>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||||
|
<style type="text/css">
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||||
|
</style>
|
||||||
|
<!--<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width: 480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
[owa] .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
table.mj-full-width-mobile {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mj-full-width-mobile {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="word-spacing: normal; background-color: #f4f4f4">
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="border-collapse:collapse;border-spacing:0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:550px;">
|
||||||
|
|
||||||
|
<a href="https://manifold.markets" target="_blank">
|
||||||
|
|
||||||
|
<img alt="banner logo" height="auto"
|
||||||
|
src="https://manifold.markets/logo-banner.png"
|
||||||
|
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||||
|
title="" width="550">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="
|
||||||
|
background: #ffffff;
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin: 0px auto;
|
||||||
|
max-width: 600px;
|
||||||
|
">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="background: #ffffff; background-color: #ffffff; width: 100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 20px 0px 0px 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="vertical-align: top" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
|
data-testid="4XoHRGw1Y"><span
|
||||||
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
</span>Hi {{name}},</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 0px;"
|
||||||
|
data-testid="4XoHRGw1Y">
|
||||||
|
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
We ran the numbers and here's how you did this past week!
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
|
||||||
|
<tr>
|
||||||
|
<tr>
|
||||||
|
<th style='font-size: 22px; text-align: center'>
|
||||||
|
Profit
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style='padding-bottom: 30px; text-align: center'>
|
||||||
|
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
|
||||||
|
{{profit}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<td align="center"
|
||||||
|
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="border-collapse:collapse;border-spacing:0px; ">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style='width: 170px'>
|
||||||
|
🔥 Prediction streak
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{prediction_streak}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
💸 Tips received
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{tips_received}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
📈 Markets traded
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{markets_traded}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
❓ Markets created
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{{markets_created}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style='width: 55px'>
|
||||||
|
🥳 Traders attracted
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{unique_bettors}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 20px; margin-bottom: 20px;"
|
||||||
|
data-testid="4XoHRGw1Y">
|
||||||
|
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
And here's some of the biggest changes in your portfolio:
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="font-size:0; padding-left:10px;padding-top:10px;padding-bottom:0;word-break:break-word;">
|
||||||
|
<table role="presentation">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class='question'>
|
||||||
|
<a class='question' href='{{question1Url}}'>
|
||||||
|
{{question1Title}}
|
||||||
|
<!-- Will the US economy recover from the pandemic?-->
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='probs'>
|
||||||
|
<p class='prob'>
|
||||||
|
{{question1Prob}}
|
||||||
|
<!-- 9.9%-->
|
||||||
|
<p class='change' style='{{question1ChangeStyle}}'>
|
||||||
|
{{question1Change}}
|
||||||
|
<!-- +17%-->
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class='question'>
|
||||||
|
<a class='question' href='{{question2Url}}'>
|
||||||
|
{{question2Title}}
|
||||||
|
<!-- Will the US economy recover from the pandemic? blah blah blah-->
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='probs'>
|
||||||
|
<p class='prob'>
|
||||||
|
{{question2Prob}}
|
||||||
|
<!-- 99.9%-->
|
||||||
|
<p class='change' style='{{question2ChangeStyle}}'>
|
||||||
|
{{question2Change}}
|
||||||
|
<!-- +7%-->
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr><tr>
|
||||||
|
<!-- <td style="{{investment_value_style}}">-->
|
||||||
|
<td class='question'>
|
||||||
|
<a class='question' href='{{question3Url}}'>
|
||||||
|
{{question3Title}}
|
||||||
|
<!-- Will the US economy recover from the pandemic?-->
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='probs'>
|
||||||
|
<p class='prob'>
|
||||||
|
{{question3Prob}}
|
||||||
|
<!-- 99.9%-->
|
||||||
|
<p class='change' style='{{question3ChangeStyle}}'>
|
||||||
|
{{question3Change}}
|
||||||
|
<!-- +17%-->
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr><tr>
|
||||||
|
<!-- <td style="{{investment_value_style}}">-->
|
||||||
|
<td class='question'>
|
||||||
|
<a class='question' href='{{question4Url}}'>
|
||||||
|
{{question4Title}}
|
||||||
|
<!-- Will the US economy recover from the pandemic?-->
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='probs'>
|
||||||
|
<p class='prob'>
|
||||||
|
{{question4Prob}}
|
||||||
|
<!-- 99.9%-->
|
||||||
|
<p class='change' style='{{question4ChangeStyle}}'>
|
||||||
|
{{question4Change}}
|
||||||
|
<!-- +17%-->
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 0 0 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="width: 100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 20px 0px 20px 0px;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align: top; padding: 0">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0"
|
||||||
|
role="presentation" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
font-family: Ubuntu, Helvetica, Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
color: #000000;
|
||||||
|
">
|
||||||
|
<p style="margin: 10px 0">
|
||||||
|
This e-mail has been sent to
|
||||||
|
{{name}},
|
||||||
|
<a href="{{unsubscribeUrl}}" style="
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
|
@ -12,14 +12,15 @@ import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||||
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||||
|
|
||||||
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||||
import { getUser } from './utils'
|
import { contractUrl, getUser } from './utils'
|
||||||
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
||||||
import { notification_reason_types } from '../../common/notification'
|
import { notification_reason_types } from '../../common/notification'
|
||||||
import { Dictionary } from 'lodash'
|
import { Dictionary } from 'lodash'
|
||||||
|
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
|
||||||
import {
|
import {
|
||||||
getNotificationDestinationsForUser,
|
PerContractInvestmentsData,
|
||||||
notification_preference,
|
OverallPerformanceData,
|
||||||
} from '../../common/user-notification-preferences'
|
} from 'functions/src/weekly-portfolio-emails'
|
||||||
|
|
||||||
export const sendMarketResolutionEmail = async (
|
export const sendMarketResolutionEmail = async (
|
||||||
reason: notification_reason_types,
|
reason: notification_reason_types,
|
||||||
|
@ -152,9 +153,10 @@ export const sendWelcomeEmail = async (
|
||||||
const { name } = user
|
const { name } = user
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
'onboarding_flow' as notification_preference
|
privateUser,
|
||||||
}`
|
'onboarding_flow'
|
||||||
|
)
|
||||||
|
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
|
@ -220,9 +222,11 @@ export const sendOneWeekBonusEmail = async (
|
||||||
const { name } = user
|
const { name } = user
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
'onboarding_flow' as notification_preference
|
privateUser,
|
||||||
}`
|
'onboarding_flow'
|
||||||
|
)
|
||||||
|
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
'Manifold Markets one week anniversary gift',
|
'Manifold Markets one week anniversary gift',
|
||||||
|
@ -252,10 +256,10 @@ export const sendCreatorGuideEmail = async (
|
||||||
|
|
||||||
const { name } = user
|
const { name } = user
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
privateUser,
|
||||||
'onboarding_flow' as notification_preference
|
'onboarding_flow'
|
||||||
}`
|
)
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
'Create your own prediction market',
|
'Create your own prediction market',
|
||||||
|
@ -286,10 +290,10 @@ export const sendThankYouEmail = async (
|
||||||
|
|
||||||
const { name } = user
|
const { name } = user
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
privateUser,
|
||||||
'thank_you_for_purchases' as notification_preference
|
'thank_you_for_purchases'
|
||||||
}`
|
)
|
||||||
|
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
|
@ -469,9 +473,10 @@ export const sendInterestingMarketsEmail = async (
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
'trending_markets' as notification_preference
|
privateUser,
|
||||||
}`
|
'trending_markets'
|
||||||
|
)
|
||||||
|
|
||||||
const { name } = user
|
const { name } = user
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
@ -507,10 +512,6 @@ export const sendInterestingMarketsEmail = async (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function contractUrl(contract: Contract) {
|
|
||||||
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function imageSourceUrl(contract: Contract) {
|
function imageSourceUrl(contract: Contract) {
|
||||||
return buildCardUrl(getOpenGraphProps(contract))
|
return buildCardUrl(getOpenGraphProps(contract))
|
||||||
}
|
}
|
||||||
|
@ -612,3 +613,47 @@ export const sendNewUniqueBettorsEmail = async (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendWeeklyPortfolioUpdateEmail = async (
|
||||||
|
user: User,
|
||||||
|
privateUser: PrivateUser,
|
||||||
|
investments: PerContractInvestmentsData[],
|
||||||
|
overallPerformance: OverallPerformanceData
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!privateUser ||
|
||||||
|
!privateUser.email ||
|
||||||
|
!privateUser.notificationPreferences.profit_loss_updates.includes('email')
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
|
privateUser,
|
||||||
|
'profit_loss_updates'
|
||||||
|
)
|
||||||
|
|
||||||
|
const { name } = user
|
||||||
|
const firstName = name.split(' ')[0]
|
||||||
|
const templateData: Record<string, string> = {
|
||||||
|
name: firstName,
|
||||||
|
unsubscribeUrl,
|
||||||
|
...overallPerformance,
|
||||||
|
}
|
||||||
|
investments.forEach((investment, i) => {
|
||||||
|
templateData[`question${i + 1}Title`] = investment.questionTitle
|
||||||
|
templateData[`question${i + 1}Url`] = investment.questionUrl
|
||||||
|
templateData[`question${i + 1}Prob`] = investment.questionProb
|
||||||
|
templateData[`question${i + 1}Change`] = investment.questionChange
|
||||||
|
templateData[`question${i + 1}ChangeStyle`] = investment.questionChangeStyle
|
||||||
|
})
|
||||||
|
|
||||||
|
await sendTemplateEmail(
|
||||||
|
privateUser.email,
|
||||||
|
// 'iansphilips@gmail.com',
|
||||||
|
`Here's your weekly portfolio update!`,
|
||||||
|
investments.length === 0
|
||||||
|
? 'portfolio-update-no-movers'
|
||||||
|
: 'portfolio-update',
|
||||||
|
templateData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ export * from './reset-betting-streaks'
|
||||||
export * from './reset-weekly-emails-flag'
|
export * from './reset-weekly-emails-flag'
|
||||||
export * from './on-update-contract-follow'
|
export * from './on-update-contract-follow'
|
||||||
export * from './on-update-like'
|
export * from './on-update-like'
|
||||||
|
export * from './weekly-portfolio-emails'
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
export * from './health'
|
export * from './health'
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const resetWeeklyEmailsFlag = functions
|
||||||
privateUsers.map(async (user) => {
|
privateUsers.map(async (user) => {
|
||||||
return firestore.collection('private-users').doc(user.id).update({
|
return firestore.collection('private-users').doc(user.id).update({
|
||||||
weeklyTrendingEmailSent: false,
|
weeklyTrendingEmailSent: false,
|
||||||
|
weeklyPortfolioEmailSent: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { stripewebhook, createcheckoutsession } from './stripe'
|
||||||
import { getcurrentuser } from './get-current-user'
|
import { getcurrentuser } from './get-current-user'
|
||||||
import { createpost } from './create-post'
|
import { createpost } from './create-post'
|
||||||
import { savetwitchcredentials } from './save-twitch-credentials'
|
import { savetwitchcredentials } from './save-twitch-credentials'
|
||||||
|
import { testscheduledfunction } from './test-scheduled-function'
|
||||||
|
|
||||||
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
||||||
const app = express()
|
const app = express()
|
||||||
|
@ -69,6 +70,7 @@ addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
||||||
addJsonEndpointRoute('/savetwitchcredentials', savetwitchcredentials)
|
addJsonEndpointRoute('/savetwitchcredentials', savetwitchcredentials)
|
||||||
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
||||||
addEndpointRoute('/createpost', createpost)
|
addEndpointRoute('/createpost', createpost)
|
||||||
|
addEndpointRoute('/testscheduledfunction', testscheduledfunction)
|
||||||
|
|
||||||
app.listen(PORT)
|
app.listen(PORT)
|
||||||
console.log(`Serving functions on port ${PORT}.`)
|
console.log(`Serving functions on port ${PORT}.`)
|
||||||
|
|
17
functions/src/test-scheduled-function.ts
Normal file
17
functions/src/test-scheduled-function.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { APIError, newEndpoint } from './api'
|
||||||
|
import { sendPortfolioUpdateEmailsToAllUsers } from './weekly-portfolio-emails'
|
||||||
|
import { isProd } from './utils'
|
||||||
|
|
||||||
|
// Function for testing scheduled functions locally
|
||||||
|
export const testscheduledfunction = newEndpoint(
|
||||||
|
{ method: 'GET', memory: '4GiB' },
|
||||||
|
async (_req) => {
|
||||||
|
if (isProd())
|
||||||
|
throw new APIError(400, 'This function is only available in dev mode')
|
||||||
|
|
||||||
|
// Replace your function here
|
||||||
|
await sendPortfolioUpdateEmailsToAllUsers()
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
)
|
|
@ -170,3 +170,7 @@ export const chargeUser = (
|
||||||
export const getContractPath = (contract: Contract) => {
|
export const getContractPath = (contract: Contract) => {
|
||||||
return `/${contract.creatorUsername}/${contract.slug}`
|
return `/${contract.creatorUsername}/${contract.slug}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function contractUrl(contract: Contract) {
|
||||||
|
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
|
||||||
|
}
|
||||||
|
|
280
functions/src/weekly-portfolio-emails.ts
Normal file
280
functions/src/weekly-portfolio-emails.ts
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { Contract, CPMMContract } from '../../common/contract'
|
||||||
|
import {
|
||||||
|
getAllPrivateUsers,
|
||||||
|
getPrivateUser,
|
||||||
|
getUser,
|
||||||
|
getValue,
|
||||||
|
getValues,
|
||||||
|
isProd,
|
||||||
|
log,
|
||||||
|
} from './utils'
|
||||||
|
import { filterDefined } from '../../common/util/array'
|
||||||
|
import { DAY_MS } from '../../common/util/time'
|
||||||
|
import { partition, sortBy, sum, uniq } from 'lodash'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
import { computeInvestmentValueCustomProb } from '../../common/calculate-metrics'
|
||||||
|
import { sendWeeklyPortfolioUpdateEmail } from './emails'
|
||||||
|
import { contractUrl } from './utils'
|
||||||
|
import { Txn } from '../../common/txn'
|
||||||
|
import { formatMoney } from '../../common/util/format'
|
||||||
|
|
||||||
|
// TODO: reset weeklyPortfolioUpdateEmailSent to false for all users at the start of each week
|
||||||
|
export const weeklyPortfolioUpdateEmails = functions
|
||||||
|
.runWith({ secrets: ['MAILGUN_KEY'], memory: '4GB' })
|
||||||
|
// every minute on Friday for an hour at 12pm PT (UTC -07:00)
|
||||||
|
.pubsub.schedule('* 19 * * 5')
|
||||||
|
.timeZone('Etc/UTC')
|
||||||
|
.onRun(async () => {
|
||||||
|
await sendPortfolioUpdateEmailsToAllUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export async function sendPortfolioUpdateEmailsToAllUsers() {
|
||||||
|
const privateUsers = isProd()
|
||||||
|
? // ian & stephen's ids
|
||||||
|
// ? filterDefined([
|
||||||
|
// await getPrivateUser('AJwLWoo3xue32XIiAVrL5SyR1WB2'),
|
||||||
|
// await getPrivateUser('tlmGNz9kjXc2EteizMORes4qvWl2'),
|
||||||
|
// ])
|
||||||
|
await getAllPrivateUsers()
|
||||||
|
: filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')])
|
||||||
|
// get all users that haven't unsubscribed from weekly emails
|
||||||
|
const privateUsersToSendEmailsTo = privateUsers
|
||||||
|
.filter((user) => {
|
||||||
|
return isProd()
|
||||||
|
? user.notificationPreferences.profit_loss_updates.includes('email') &&
|
||||||
|
!user.weeklyPortfolioUpdateEmailSent
|
||||||
|
: true
|
||||||
|
})
|
||||||
|
// Send emails in batches
|
||||||
|
.slice(0, 200)
|
||||||
|
log(
|
||||||
|
'Sending weekly portfolio emails to',
|
||||||
|
privateUsersToSendEmailsTo.length,
|
||||||
|
'users'
|
||||||
|
)
|
||||||
|
|
||||||
|
const usersBets: { [userId: string]: Bet[] } = {}
|
||||||
|
// get all bets made by each user
|
||||||
|
await Promise.all(
|
||||||
|
privateUsersToSendEmailsTo.map(async (user) => {
|
||||||
|
return getValues<Bet>(
|
||||||
|
firestore.collectionGroup('bets').where('userId', '==', user.id)
|
||||||
|
).then((bets) => {
|
||||||
|
usersBets[user.id] = bets
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const usersToContractsCreated: { [userId: string]: Contract[] } = {}
|
||||||
|
// Get all contracts created by each user
|
||||||
|
await Promise.all(
|
||||||
|
privateUsersToSendEmailsTo.map(async (user) => {
|
||||||
|
return getValues<Contract>(
|
||||||
|
firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('creatorId', '==', user.id)
|
||||||
|
.where('createdTime', '>', Date.now() - 7 * DAY_MS)
|
||||||
|
).then((contracts) => {
|
||||||
|
usersToContractsCreated[user.id] = contracts
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get all txns the users received over the past week
|
||||||
|
const usersToTxnsReceived: { [userId: string]: Txn[] } = {}
|
||||||
|
await Promise.all(
|
||||||
|
privateUsersToSendEmailsTo.map(async (user) => {
|
||||||
|
return getValues<Txn>(
|
||||||
|
firestore
|
||||||
|
.collection(`txns`)
|
||||||
|
.where('toId', '==', user.id)
|
||||||
|
.where('createdTime', '>', Date.now() - 7 * DAY_MS)
|
||||||
|
).then((txn) => {
|
||||||
|
usersToTxnsReceived[user.id] = txn
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get a flat map of all the bets that users made to get the contracts they bet on
|
||||||
|
const contractsUsersBetOn = filterDefined(
|
||||||
|
await Promise.all(
|
||||||
|
uniq(
|
||||||
|
Object.values(usersBets).flatMap((bets) =>
|
||||||
|
bets.map((bet) => bet.contractId)
|
||||||
|
)
|
||||||
|
).map((contractId) =>
|
||||||
|
getValue<Contract>(firestore.collection('contracts').doc(contractId))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
log('Found', contractsUsersBetOn.length, 'contracts')
|
||||||
|
let count = 0
|
||||||
|
await Promise.all(
|
||||||
|
privateUsersToSendEmailsTo.map(async (privateUser) => {
|
||||||
|
const user = await getUser(privateUser.id)
|
||||||
|
if (!user) return
|
||||||
|
const userBets = usersBets[privateUser.id] as Bet[]
|
||||||
|
const contractsUserBetOn = contractsUsersBetOn.filter((contract) =>
|
||||||
|
userBets.some((bet) => bet.contractId === contract.id)
|
||||||
|
)
|
||||||
|
const contractsBetOnInLastWeek = uniq(
|
||||||
|
userBets
|
||||||
|
.filter((bet) => bet.createdTime > Date.now() - 7 * DAY_MS)
|
||||||
|
.map((bet) => bet.contractId)
|
||||||
|
)
|
||||||
|
const totalTips = sum(
|
||||||
|
usersToTxnsReceived[privateUser.id]
|
||||||
|
.filter((txn) => txn.category === 'TIP')
|
||||||
|
.map((txn) => txn.amount)
|
||||||
|
)
|
||||||
|
const greenBg = 'rgba(0,160,0,0.2)'
|
||||||
|
const redBg = 'rgba(160,0,0,0.2)'
|
||||||
|
const clearBg = 'rgba(255,255,255,0)'
|
||||||
|
const roundedProfit =
|
||||||
|
Math.round(user.profitCached.weekly) === 0
|
||||||
|
? 0
|
||||||
|
: Math.floor(user.profitCached.weekly)
|
||||||
|
const performanceData = {
|
||||||
|
profit: formatMoney(user.profitCached.weekly),
|
||||||
|
profit_style: `background-color: ${
|
||||||
|
roundedProfit > 0 ? greenBg : roundedProfit === 0 ? clearBg : redBg
|
||||||
|
}`,
|
||||||
|
markets_created:
|
||||||
|
usersToContractsCreated[privateUser.id].length.toString(),
|
||||||
|
tips_received: formatMoney(totalTips),
|
||||||
|
unique_bettors: usersToTxnsReceived[privateUser.id]
|
||||||
|
.filter((txn) => txn.category === 'UNIQUE_BETTOR_BONUS')
|
||||||
|
.length.toString(),
|
||||||
|
markets_traded: contractsBetOnInLastWeek.length.toString(),
|
||||||
|
prediction_streak:
|
||||||
|
(user.currentBettingStreak?.toString() ?? '0') + ' days',
|
||||||
|
// More options: bonuses, tips given,
|
||||||
|
} as OverallPerformanceData
|
||||||
|
|
||||||
|
const investmentValueDifferences = sortBy(
|
||||||
|
filterDefined(
|
||||||
|
contractsUserBetOn.map((contract) => {
|
||||||
|
const cpmmContract = contract as CPMMContract
|
||||||
|
if (cpmmContract === undefined || cpmmContract.prob === undefined)
|
||||||
|
return
|
||||||
|
const bets = userBets.filter(
|
||||||
|
(bet) => bet.contractId === contract.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const marketProbabilityAWeekAgo =
|
||||||
|
cpmmContract.prob - cpmmContract.probChanges.week
|
||||||
|
const currentMarketProbability = cpmmContract.resolutionProbability
|
||||||
|
? cpmmContract.resolutionProbability
|
||||||
|
: cpmmContract.prob
|
||||||
|
const betsValueAWeekAgo = computeInvestmentValueCustomProb(
|
||||||
|
bets.filter((b) => b.createdTime < Date.now() - 7 * DAY_MS),
|
||||||
|
contract,
|
||||||
|
marketProbabilityAWeekAgo
|
||||||
|
)
|
||||||
|
const currentBetsValue = computeInvestmentValueCustomProb(
|
||||||
|
bets,
|
||||||
|
contract,
|
||||||
|
currentMarketProbability
|
||||||
|
)
|
||||||
|
const marketChange =
|
||||||
|
currentMarketProbability - marketProbabilityAWeekAgo
|
||||||
|
return {
|
||||||
|
currentValue: currentBetsValue,
|
||||||
|
pastValue: betsValueAWeekAgo,
|
||||||
|
difference: currentBetsValue - betsValueAWeekAgo,
|
||||||
|
contractSlug: contract.slug,
|
||||||
|
marketProbAWeekAgo: marketProbabilityAWeekAgo,
|
||||||
|
questionTitle: contract.question,
|
||||||
|
questionUrl: contractUrl(contract),
|
||||||
|
questionProb: cpmmContract.resolution
|
||||||
|
? cpmmContract.resolution
|
||||||
|
: Math.round(cpmmContract.prob * 100) + '%',
|
||||||
|
questionChange:
|
||||||
|
(marketChange > 0 ? '+' : '') +
|
||||||
|
Math.round(marketChange * 100) +
|
||||||
|
'%',
|
||||||
|
questionChangeStyle: `color: ${
|
||||||
|
currentMarketProbability > marketProbabilityAWeekAgo
|
||||||
|
? 'rgba(0,160,0,1)'
|
||||||
|
: '#a80000'
|
||||||
|
};`,
|
||||||
|
} as PerContractInvestmentsData
|
||||||
|
})
|
||||||
|
),
|
||||||
|
(differences) => Math.abs(differences.difference)
|
||||||
|
).reverse()
|
||||||
|
|
||||||
|
log(
|
||||||
|
'Found',
|
||||||
|
investmentValueDifferences.length,
|
||||||
|
'investment differences for user',
|
||||||
|
privateUser.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const [winningInvestments, losingInvestments] = partition(
|
||||||
|
investmentValueDifferences.filter(
|
||||||
|
(diff) =>
|
||||||
|
diff.pastValue > 0.01 &&
|
||||||
|
Math.abs(diff.difference / diff.pastValue) > 0.01 // difference is greater than 1%
|
||||||
|
),
|
||||||
|
(investmentsData: PerContractInvestmentsData) => {
|
||||||
|
return investmentsData.difference > 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// pick 3 winning investments and 3 losing investments
|
||||||
|
const topInvestments = winningInvestments.slice(0, 2)
|
||||||
|
const worstInvestments = losingInvestments.slice(0, 2)
|
||||||
|
// if no bets in the last week ANd no market movers AND no markets created, don't send email
|
||||||
|
if (
|
||||||
|
contractsBetOnInLastWeek.length === 0 &&
|
||||||
|
topInvestments.length === 0 &&
|
||||||
|
worstInvestments.length === 0 &&
|
||||||
|
usersToContractsCreated[privateUser.id].length === 0
|
||||||
|
) {
|
||||||
|
log('No bets in last week, no market movers, no markets created')
|
||||||
|
await firestore.collection('private-users').doc(privateUser.id).update({
|
||||||
|
weeklyPortfolioUpdateEmailSent: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await sendWeeklyPortfolioUpdateEmail(
|
||||||
|
user,
|
||||||
|
privateUser,
|
||||||
|
topInvestments.concat(worstInvestments) as PerContractInvestmentsData[],
|
||||||
|
performanceData
|
||||||
|
)
|
||||||
|
await firestore.collection('private-users').doc(privateUser.id).update({
|
||||||
|
weeklyPortfolioUpdateEmailSent: true,
|
||||||
|
})
|
||||||
|
log('Sent weekly portfolio update email to', privateUser.email)
|
||||||
|
count++
|
||||||
|
log('sent out emails to user count:', count)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PerContractInvestmentsData = {
|
||||||
|
questionTitle: string
|
||||||
|
questionUrl: string
|
||||||
|
questionProb: string
|
||||||
|
questionChange: string
|
||||||
|
questionChangeStyle: string
|
||||||
|
currentValue: number
|
||||||
|
pastValue: number
|
||||||
|
difference: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OverallPerformanceData = {
|
||||||
|
profit: string
|
||||||
|
prediction_streak: string
|
||||||
|
markets_traded: string
|
||||||
|
profit_style: string
|
||||||
|
tips_received: string
|
||||||
|
markets_created: string
|
||||||
|
unique_bettors: string
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user