Add paypal checkout
This commit is contained in:
166
svelte/src/admin_panel/InvoiceVAT.svelte
Normal file
166
svelte/src/admin_panel/InvoiceVAT.svelte
Normal file
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate } from "util/Formatting";
|
||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||
|
||||
type Invoice = {
|
||||
id: string
|
||||
time: string
|
||||
amount: number
|
||||
vat: number
|
||||
country: string
|
||||
payment_method: string
|
||||
status: string
|
||||
}
|
||||
|
||||
let loading = true
|
||||
let invoices: Invoice[] = []
|
||||
|
||||
let year = 0
|
||||
let month = 0
|
||||
let month_str = ""
|
||||
|
||||
type Total = {
|
||||
count: number
|
||||
amount: number
|
||||
vat: number
|
||||
}
|
||||
let totals: { [id: string]: Total } = {}
|
||||
const add_total = (i: Invoice) => {
|
||||
if (totals[i.payment_method] === undefined) {
|
||||
totals[i.payment_method] = {count: 0, amount: 0, vat: 0}
|
||||
}
|
||||
|
||||
totals[i.payment_method].count++
|
||||
totals[i.payment_method].amount += i.amount
|
||||
totals[i.payment_method].vat += i.vat
|
||||
}
|
||||
|
||||
const get_invoices = async () => {
|
||||
loading = true;
|
||||
month_str = year + "-" + ("00"+(month)).slice(-2)
|
||||
try {
|
||||
const resp = await fetch(get_endpoint()+"/admin/invoices/"+month_str);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
|
||||
let resp_json = await resp.json() as Invoice[];
|
||||
|
||||
resp_json.sort((a, b) => {
|
||||
if (a.status !== b.status) {
|
||||
return a.status.localeCompare(b.status)
|
||||
}
|
||||
|
||||
const date_a = new Date(a.time)
|
||||
const date_b = new Date(b.time)
|
||||
return date_a.getTime() - date_b.getTime()
|
||||
})
|
||||
|
||||
totals = {}
|
||||
resp_json.forEach(row => {
|
||||
if (row.status === "paid") {
|
||||
add_total(row)
|
||||
}
|
||||
if (row.status === "chargeback") {
|
||||
alert(row.vat)
|
||||
}
|
||||
});
|
||||
|
||||
invoices = resp_json
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const last_month = () => {
|
||||
month--
|
||||
if (month === 0) {
|
||||
month = 12
|
||||
year--
|
||||
}
|
||||
|
||||
get_invoices()
|
||||
}
|
||||
const next_month = () => {
|
||||
month++
|
||||
if (month === 13) {
|
||||
month = 1
|
||||
year++
|
||||
}
|
||||
|
||||
get_invoices()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let now = new Date()
|
||||
year = now.getFullYear()
|
||||
month = now.getMonth()+1
|
||||
get_invoices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<section>
|
||||
<h3>{month_str}</h3>
|
||||
<div class="toolbar">
|
||||
<button on:click={last_month}>
|
||||
<i class="icon">chevron_left</i>
|
||||
Previous month
|
||||
</button>
|
||||
<div class="toolbar_spacer"></div>
|
||||
<button on:click={next_month}>
|
||||
Next month
|
||||
<i class="icon">chevron_right</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#each Object.entries(totals) as [key, tot]}
|
||||
{key} ({tot.count})<br/>
|
||||
Amount:<Euro amount={tot.amount}/><br/>
|
||||
VAT: <Euro amount={tot.vat}/><br/>
|
||||
{/each}
|
||||
|
||||
<div class="table_scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td>Amount</td>
|
||||
<td>VAT</td>
|
||||
<td>Country</td>
|
||||
<td>Method</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each invoices as row (row.id)}
|
||||
<tr>
|
||||
<td>{formatDate(row.time)}</td>
|
||||
<td><Euro amount={row.amount}/></td>
|
||||
<td><Euro amount={row.vat}/></td>
|
||||
<td>{row.country}</td>
|
||||
<td>{row.payment_method}</td>
|
||||
<td>{row.status}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
.toolbar > * { flex: 0 0 auto; }
|
||||
.toolbar_spacer { flex: 1 1 auto; }
|
||||
</style>
|
@@ -9,6 +9,7 @@ import EmailReporters from "./EmailReporters.svelte";
|
||||
import MollieSettlements from "./MollieSettlements.svelte";
|
||||
import PayPalTaxes from "./PayPalTaxes.svelte";
|
||||
import UserBans from "./user_bans/UserBans.svelte";
|
||||
import InvoiceVat from "./InvoiceVAT.svelte";
|
||||
|
||||
let pages = [
|
||||
{
|
||||
@@ -62,6 +63,11 @@ let pages = [
|
||||
title: "Paypal Taxes",
|
||||
icon: "paypal",
|
||||
component: PayPalTaxes,
|
||||
}, {
|
||||
path: "/admin/invoices",
|
||||
title: "Invoices",
|
||||
icon: "receipt",
|
||||
component: InvoiceVat,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import CreditDeposit from "layout/CreditDeposit.svelte";
|
||||
import LoginRegister from "login/LoginRegister.svelte";
|
||||
import MollieDeposit from "user_home/MollieDeposit.svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
</script>
|
||||
|
||||
@@ -19,13 +19,13 @@ import Euro from "util/Euro.svelte";
|
||||
balance is <Euro amount={window.user.balance_micro_eur}/>. Use
|
||||
the form below to top up your balance.
|
||||
</p>
|
||||
<MollieDeposit/>
|
||||
<CreditDeposit/>
|
||||
{:else}
|
||||
<p>
|
||||
You are currently logged in as '{window.user.username}'. Use the
|
||||
form below to activate prepaid on this account.
|
||||
</p>
|
||||
<MollieDeposit/>
|
||||
<CreditDeposit/>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
@@ -4,11 +4,13 @@ import Euro from "util/Euro.svelte";
|
||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
||||
import { countries } from "country-data-list";
|
||||
import { get_endpoint, get_misc_vat_rate } from "lib/PixeldrainAPI";
|
||||
import CreditDepositNav from "./CreditDepositNav.svelte";
|
||||
|
||||
let loading = false
|
||||
let amount = 20
|
||||
let country: typeof countries.all[0] = null
|
||||
let country_input = ""
|
||||
let provider: PaymentProvider = null
|
||||
let vat = 0
|
||||
|
||||
const amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000]
|
||||
@@ -19,8 +21,30 @@ onMount(() => {
|
||||
country_input = checkout_country
|
||||
set_country()
|
||||
}
|
||||
|
||||
const checkout_provider = window.localStorage.getItem("checkout_provider")
|
||||
for (const p of providers) {
|
||||
if (p.name === checkout_provider) {
|
||||
set_provider(p)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
type PaymentProvider = {
|
||||
icon: string,
|
||||
name: string,
|
||||
label: string,
|
||||
crypto?: boolean,
|
||||
};
|
||||
const providers: PaymentProvider[] = [
|
||||
{icon: "paypal_full", name: "paypal", label: "PayPal"},
|
||||
{icon: "mollie", name: "mollie", label: "Mollie"},
|
||||
{icon: "bitcoin", name: "btc", label: "Bitcoin", crypto: true},
|
||||
{icon: "dogecoin", name: "doge", label: "Dogecoin", crypto: true},
|
||||
{icon: "monero", name: "xmr", label: "Monero", crypto: true},
|
||||
]
|
||||
|
||||
const payment_providers = [
|
||||
{icon: "paypal", name: "PayPal"},
|
||||
{icon: "creditcard", name: "Credit/debit"},
|
||||
@@ -69,6 +93,13 @@ const set_country = async (e?: Event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const set_provider = (p: PaymentProvider) => {
|
||||
provider = p
|
||||
|
||||
// Cache the value for next checkout
|
||||
window.localStorage.setItem("checkout_provider", p.name)
|
||||
}
|
||||
|
||||
const checkout = async () => {
|
||||
loading = true
|
||||
|
||||
@@ -79,7 +110,7 @@ const checkout = async () => {
|
||||
|
||||
const form = new FormData()
|
||||
form.set("amount", String(amount*1e6))
|
||||
form.set("network", "mollie")
|
||||
form.set("network", provider.name)
|
||||
form.set("country", country.alpha2)
|
||||
|
||||
try {
|
||||
@@ -161,21 +192,32 @@ const format_country = (c: typeof countries.all[0]) => {
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
{:else if provider === null}
|
||||
<CreditDepositNav bind:country={country} bind:provider={provider} bind:vat={vat}/>
|
||||
|
||||
<div style="display: flex;">
|
||||
<button on:click={() => country = null} style="flex: 0 0 auto;">
|
||||
<i class="icon">chevron_left</i>
|
||||
Change country
|
||||
</button>
|
||||
<div style="flex: 1 1 auto;"></div>
|
||||
<div style="flex: 0 0 auto; display: flex; gap: 0.25em; align-items: center;">
|
||||
<span>Paying from</span>
|
||||
<span style="font-size: 1.5em; line-height: 1em;">{country.emoji}</span>
|
||||
<span>{country.name} ({vat}% VAT)</span>
|
||||
</div>
|
||||
<h2>Please select a payment provider</h2>
|
||||
|
||||
<div class="providers">
|
||||
{#each providers as p (p.name)}
|
||||
<button on:click={() => set_provider(p)}>
|
||||
<img src="/res/img/payment_providers/{p.icon}.svg" alt={p.label} title={p.label}/>
|
||||
{p.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<CreditDepositNav bind:country={country} bind:provider={provider} bind:vat={vat}/>
|
||||
|
||||
<p style="text-align: initial;" class="highlight_blue">
|
||||
When paying with cryptocurrencies it is important that you pay the
|
||||
<b>exact amount</b> stated on the order. If you pay too little, the
|
||||
order fails. If you pay too much then the remaining credit will not
|
||||
be added to your account. Pay close attention when sending a payment
|
||||
from an online exchange, sometimes they will subtract the fees from
|
||||
the amount sent which will cause the payment to fail.
|
||||
</p>
|
||||
|
||||
<form class="amount_grid" on:submit|preventDefault={checkout}>
|
||||
<div class="span3">Please choose an amount</div>
|
||||
{#each amounts as a}
|
||||
@@ -237,6 +279,19 @@ const format_country = (c: typeof countries.all[0]) => {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.providers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(10em, 1fr));
|
||||
}
|
||||
.providers > button {
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.providers > button > img {
|
||||
max-width: 3em;
|
||||
max-height: 3em;
|
||||
}
|
||||
|
||||
.amount_grid {
|
||||
max-width: 500px;
|
||||
gap: 4px;
|
29
svelte/src/layout/CreditDepositNav.svelte
Normal file
29
svelte/src/layout/CreditDepositNav.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
export let country: {name?: string, emoji?: string} = null
|
||||
export let provider: {label: string} = null
|
||||
export let vat = 0
|
||||
</script>
|
||||
|
||||
<div style="display: flex;">
|
||||
{#if provider !== null}
|
||||
<button on:click={() => provider = null} style="flex: 0 0 auto;">
|
||||
<i class="icon">chevron_left</i>
|
||||
Change provider
|
||||
</button>
|
||||
{:else if country !== null}
|
||||
<button on:click={() => country = null} style="flex: 0 0 auto;">
|
||||
<i class="icon">chevron_left</i>
|
||||
Change country
|
||||
</button>
|
||||
{/if}
|
||||
<div style="flex: 1 1 auto;"></div>
|
||||
<div style="flex: 0 0 auto; display: flex; gap: 0.25em; align-items: center;">
|
||||
<span>Paying from</span>
|
||||
<span style="font-size: 1.5em; line-height: 1em;">{country.emoji}</span>
|
||||
<span>
|
||||
{country.name}
|
||||
({vat}% VAT)
|
||||
{#if provider !== null}with {provider.label}{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
@@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import CreditDeposit from "layout/CreditDeposit.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { formatDate } from "util/Formatting";
|
||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
||||
import MollieDeposit from "./MollieDeposit.svelte";
|
||||
|
||||
let loading = false
|
||||
let credit_amount = 10
|
||||
let tab = "mollie"
|
||||
|
||||
const checkout = async (network = "", amount = 0, country = "") => {
|
||||
loading = true
|
||||
@@ -55,7 +54,7 @@ const load_invoices = async () => {
|
||||
invoices_tmp.forEach(row => {
|
||||
row.time = new Date(row.time)
|
||||
|
||||
if (row.payment_method === "mollie" && row.status === "open") {
|
||||
if (row.status === "open") {
|
||||
unpaid_invoice = true
|
||||
}
|
||||
})
|
||||
@@ -89,61 +88,18 @@ onMount(() => {
|
||||
amount={window.user.balance_micro_eur}/>
|
||||
</p>
|
||||
|
||||
<div class="tab_bar">
|
||||
<button on:click={() => tab = "mollie"} class:button_highlight={tab === "mollie"}>
|
||||
<i class="icon">euro</i>
|
||||
Mollie
|
||||
</button>
|
||||
<button on:click={() => tab = "btcpay"} class:button_highlight={tab === "btcpay"}>
|
||||
<i class="icon">currency_bitcoin</i>
|
||||
Crypto
|
||||
</button>
|
||||
</div>
|
||||
{#if tab === "mollie"}
|
||||
{#if unpaid_invoice}
|
||||
<div class="highlight_yellow">
|
||||
<p>
|
||||
You still have an unpaid invoice open. Please pay that one
|
||||
before requesting a new invoice. You can find unpaid
|
||||
invoices at the bottom of this page. You can cancel an
|
||||
invoice by clicking Pay, and then clicking the Back link at
|
||||
the bottom of the page.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<MollieDeposit/>
|
||||
{/if}
|
||||
{:else if tab === "btcpay"}
|
||||
<div class="highlight_border">
|
||||
<p style="text-align: initial">
|
||||
Alternatively you can use Bitcoin, Lightning network (<a
|
||||
href="https://btcpay.pixeldrain.com/embed/uS2mbWjXUuaAqMh8XLjkjwi8oehFuxeBZxekMxv68LN/BTC/ln"
|
||||
target="_blank" rel="noreferrer">node info</a>) and Dogecoin to deposit
|
||||
credits on your pixeldrain account. You must pay the full amount as
|
||||
stated on the invoice, else your payment will fail.
|
||||
{#if unpaid_invoice}
|
||||
<div class="highlight_yellow">
|
||||
<p>
|
||||
You still have an unpaid invoice open. Please pay that one
|
||||
before requesting a new invoice. You can find unpaid
|
||||
invoices at the bottom of this page. You can cancel an
|
||||
invoice by clicking Pay, and then clicking the Back link at
|
||||
the bottom of the page.
|
||||
</p>
|
||||
<p style="text-align: initial">
|
||||
Do note that it is not possible to withdraw coins from your
|
||||
pixeldrain account. It's not a wallet. Any amount of money you
|
||||
deposit has to be used up.
|
||||
</p>
|
||||
Deposit amount €
|
||||
<input type="number" bind:value={credit_amount} min="10"/>
|
||||
<br/>
|
||||
Choose payment method:<br/>
|
||||
<button on:click={() => {checkout("btc", credit_amount)}}>
|
||||
<i class="icon">currency_bitcoin</i> Bitcoin
|
||||
</button>
|
||||
<button on:click={() => {checkout("btc_lightning", credit_amount)}}>
|
||||
<i class="icon">bolt</i> Lightning network
|
||||
</button>
|
||||
<button on:click={() => {checkout("doge", credit_amount)}}>
|
||||
<span class="icon_unicode">Ð</span> Dogecoin
|
||||
</button>
|
||||
<button on:click={() => {checkout("xmr", credit_amount)}}>
|
||||
<span class="icon_unicode">M</span> Monero
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<CreditDeposit/>
|
||||
{/if}
|
||||
|
||||
<h3 id="invoices">Past invoices</h3>
|
||||
@@ -174,7 +130,7 @@ onMount(() => {
|
||||
<td>{row.country}</td>
|
||||
<td>{row.payment_method}</td>
|
||||
<td>
|
||||
{#if row.status === "InvoiceCreated" || row.status === "open"}
|
||||
{#if row.status === "InvoiceCreated" || row.status === "open" || row.status === "CREATED" || row.status === "PAYER_ACTION_REQUIRED"}
|
||||
Waiting for payment
|
||||
{:else if row.status === "InvoiceProcessing"}
|
||||
Payment received, waiting for confirmations
|
||||
@@ -189,7 +145,12 @@ onMount(() => {
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if row.status === "New" || row.status === "InvoiceCreated" || row.status === "open"}
|
||||
{#if row.status === "New" ||
|
||||
row.status === "InvoiceCreated" ||
|
||||
row.status === "open" ||
|
||||
row.status === "CREATED" ||
|
||||
row.status === "PAYER_ACTION_REQUIRED"
|
||||
}
|
||||
<a href="/api/user/pay_invoice/{row.id}" class="button button_highlight">
|
||||
<i class="icon">paid</i> Pay
|
||||
</a>
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import Euro from "util/Euro.svelte"
|
||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
||||
import SuccessMessage from "util/SuccessMessage.svelte";
|
||||
@@ -47,20 +46,12 @@ const update = async (plan) => {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let checkout_success = false
|
||||
|
||||
onMount(() => {
|
||||
if (window.location.hash === "#checkout_complete") {
|
||||
checkout_success = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<section>
|
||||
{#if checkout_success}
|
||||
{#if window.location.hash === "#checkout_complete"}
|
||||
<div class="highlight_green">
|
||||
<h2>Payment successful!</h2>
|
||||
<p>
|
||||
@@ -78,6 +69,24 @@ onMount(() => {
|
||||
support@pixeldrain.com.
|
||||
</p>
|
||||
</div>
|
||||
{:else if window.location.hash === "#order_expired"}
|
||||
<div class="highlight_yellow">
|
||||
<h2>Order expired</h2>
|
||||
<p>
|
||||
This order has expired. Please create a new order on the <a
|
||||
href="/user/prepaid/deposit">credit deposit page</a>.
|
||||
</p>
|
||||
</div>
|
||||
{:else if window.location.hash === "#order_canceled"}
|
||||
<div class="highlight_yellow">
|
||||
<h2>Order canceled</h2>
|
||||
<p>
|
||||
You have chosen to cancel the order. If you still want to
|
||||
proceed with the order you can initiate payment again from the
|
||||
<a href="/user/prepaid/deposit">deposit page</a>. If not then
|
||||
you can let the invoice expire.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<PatreonActivationResult/>
|
||||
|
Reference in New Issue
Block a user