mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-10 09:19:42 +01:00
remove invoices (#1515)
* remove invoices * remove invoices tests * remove unused tests/extensions dir * fix issue * formatting * test passing locally. and i dont know why * fix _extension_dir loading * gitkeep extension dir * formatting * remove bundle from check * do not prettier compliled css * do not prettier compliled css * create extension dir, if it does not exists, just to be sure * Update lnbits/extension_manager.py --------- Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
parent
bb05703f2b
commit
ca75b328a1
23 changed files with 152 additions and 2160 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -44,5 +44,5 @@ docker
|
||||||
fly.toml
|
fly.toml
|
||||||
|
|
||||||
# Ignore extensions (post installable extension PR)
|
# Ignore extensions (post installable extension PR)
|
||||||
extensions/
|
extensions/*
|
||||||
upgrades/
|
upgrades/
|
|
@ -5,4 +5,6 @@
|
||||||
|
|
||||||
*.yml
|
*.yml
|
||||||
|
|
||||||
**/lnbits/static
|
**/lnbits/static/vendor
|
||||||
|
**/lnbits/static/bundle.*
|
||||||
|
**/lnbits/static/css/*
|
||||||
|
|
|
@ -51,13 +51,16 @@ class Extension(NamedTuple):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# All subdirectories in the current directory, not recursive.
|
||||||
|
|
||||||
|
|
||||||
class ExtensionManager:
|
class ExtensionManager:
|
||||||
def __init__(self, include_disabled_exts=False):
|
def __init__(self, include_disabled_exts=False):
|
||||||
self._disabled: List[str] = settings.lnbits_disabled_extensions
|
self._disabled: List[str] = settings.lnbits_disabled_extensions
|
||||||
self._admin_only: List[str] = settings.lnbits_admin_extensions
|
self._admin_only: List[str] = settings.lnbits_admin_extensions
|
||||||
self._extension_folders: List[str] = [
|
p = Path(settings.lnbits_path, "extensions")
|
||||||
x[1] for x in os.walk(os.path.join(settings.lnbits_path, "extensions"))
|
os.makedirs(p, exist_ok=True)
|
||||||
][0]
|
self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extensions(self) -> List[Extension]:
|
def extensions(self) -> List[Extension]:
|
||||||
|
@ -70,11 +73,7 @@ class ExtensionManager:
|
||||||
ext for ext in self._extension_folders if ext not in self._disabled
|
ext for ext in self._extension_folders if ext not in self._disabled
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
with open(
|
with open(extension / "config.json") as json_file:
|
||||||
os.path.join(
|
|
||||||
settings.lnbits_path, "extensions", extension, "config.json"
|
|
||||||
)
|
|
||||||
) as json_file:
|
|
||||||
config = json.load(json_file)
|
config = json.load(json_file)
|
||||||
is_valid = True
|
is_valid = True
|
||||||
is_admin_only = True if extension in self._admin_only else False
|
is_admin_only = True if extension in self._admin_only else False
|
||||||
|
@ -83,9 +82,10 @@ class ExtensionManager:
|
||||||
is_valid = False
|
is_valid = False
|
||||||
is_admin_only = False
|
is_admin_only = False
|
||||||
|
|
||||||
|
*_, extension_code = extension.parts
|
||||||
output.append(
|
output.append(
|
||||||
Extension(
|
Extension(
|
||||||
extension,
|
extension_code,
|
||||||
is_valid,
|
is_valid,
|
||||||
is_admin_only,
|
is_admin_only,
|
||||||
config.get("name"),
|
config.get("name"),
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Invoices
|
|
||||||
|
|
||||||
## Create invoices that you can send to your client to pay online over Lightning.
|
|
||||||
|
|
||||||
This extension allows users to create "traditional" invoices (not in the lightning sense) that contain one or more line items. Line items are denominated in a user-configurable fiat currency. Each invoice contains one or more payments up to the total of the invoice. Each invoice creates a public link that can be shared with a customer that they can use to (partially or in full) pay the invoice.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Create an invoice by clicking "NEW INVOICE"\
|
|
||||||

|
|
||||||
2. Fill the options for your INVOICE
|
|
||||||
- select the wallet
|
|
||||||
- select the fiat currency the invoice will be denominated in
|
|
||||||
- select a status for the invoice (default is draft)
|
|
||||||
- enter a company name, first name, last name, email, phone & address (optional)
|
|
||||||
- add one or more line items
|
|
||||||
- enter a name & price for each line item
|
|
||||||
3. You can then use share your invoice link with your customer to receive payment\
|
|
||||||

|
|
|
@ -1,36 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from starlette.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from lnbits.db import Database
|
|
||||||
from lnbits.helpers import template_renderer
|
|
||||||
from lnbits.tasks import catch_everything_and_restart
|
|
||||||
|
|
||||||
db = Database("ext_invoices")
|
|
||||||
|
|
||||||
invoices_static_files = [
|
|
||||||
{
|
|
||||||
"path": "/invoices/static",
|
|
||||||
"app": StaticFiles(directory="lnbits/extensions/invoices/static"),
|
|
||||||
"name": "invoices_static",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
invoices_ext: APIRouter = APIRouter(prefix="/invoices", tags=["invoices"])
|
|
||||||
|
|
||||||
|
|
||||||
def invoices_renderer():
|
|
||||||
return template_renderer(["lnbits/extensions/invoices/templates"])
|
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_paid_invoices
|
|
||||||
|
|
||||||
|
|
||||||
def invoices_start():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
|
||||||
|
|
||||||
|
|
||||||
from .views import * # noqa: F401,F403
|
|
||||||
from .views_api import * # noqa: F401,F403
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Invoices",
|
|
||||||
"short_description": "Create invoices for your clients.",
|
|
||||||
"tile": "/invoices/static/image/invoices.png",
|
|
||||||
"contributors": ["leesalminen"]
|
|
||||||
}
|
|
|
@ -1,210 +0,0 @@
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
|
|
||||||
from . import db
|
|
||||||
from .models import (
|
|
||||||
CreateInvoiceData,
|
|
||||||
CreateInvoiceItemData,
|
|
||||||
Invoice,
|
|
||||||
InvoiceItem,
|
|
||||||
Payment,
|
|
||||||
UpdateInvoiceData,
|
|
||||||
UpdateInvoiceItemData,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice(invoice_id: str) -> Optional[Invoice]:
|
|
||||||
row = await db.fetchone(
|
|
||||||
"SELECT * FROM invoices.invoices WHERE id = ?", (invoice_id,)
|
|
||||||
)
|
|
||||||
return Invoice.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice_items(invoice_id: str) -> List[InvoiceItem]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT * FROM invoices.invoice_items WHERE invoice_id = ?", (invoice_id,)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [InvoiceItem.from_row(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice_item(item_id: str) -> Optional[InvoiceItem]:
|
|
||||||
row = await db.fetchone(
|
|
||||||
"SELECT * FROM invoices.invoice_items WHERE id = ?", (item_id,)
|
|
||||||
)
|
|
||||||
return InvoiceItem.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice_total(items: List[InvoiceItem]) -> int:
|
|
||||||
return sum(item.amount for item in items)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_invoices(wallet_ids: Union[str, List[str]]) -> List[Invoice]:
|
|
||||||
if isinstance(wallet_ids, str):
|
|
||||||
wallet_ids = [wallet_ids]
|
|
||||||
|
|
||||||
q = ",".join(["?"] * len(wallet_ids))
|
|
||||||
rows = await db.fetchall(
|
|
||||||
f"SELECT * FROM invoices.invoices WHERE wallet IN ({q})", (*wallet_ids,)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [Invoice.from_row(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice_payments(invoice_id: str) -> List[Payment]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT * FROM invoices.payments WHERE invoice_id = ?", (invoice_id,)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [Payment.from_row(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice_payment(payment_id: str) -> Optional[Payment]:
|
|
||||||
row = await db.fetchone(
|
|
||||||
"SELECT * FROM invoices.payments WHERE id = ?", (payment_id,)
|
|
||||||
)
|
|
||||||
return Payment.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_payments_total(payments: List[Payment]) -> int:
|
|
||||||
return sum(item.amount for item in payments)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_invoice_internal(wallet_id: str, data: CreateInvoiceData) -> Invoice:
|
|
||||||
invoice_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO invoices.invoices (id, wallet, status, currency, company_name, first_name, last_name, email, phone, address)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
invoice_id,
|
|
||||||
wallet_id,
|
|
||||||
data.status,
|
|
||||||
data.currency,
|
|
||||||
data.company_name,
|
|
||||||
data.first_name,
|
|
||||||
data.last_name,
|
|
||||||
data.email,
|
|
||||||
data.phone,
|
|
||||||
data.address,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice = await get_invoice(invoice_id)
|
|
||||||
assert invoice, "Newly created invoice couldn't be retrieved"
|
|
||||||
return invoice
|
|
||||||
|
|
||||||
|
|
||||||
async def create_invoice_items(
|
|
||||||
invoice_id: str, data: List[CreateInvoiceItemData]
|
|
||||||
) -> List[InvoiceItem]:
|
|
||||||
for item in data:
|
|
||||||
item_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO invoices.invoice_items (id, invoice_id, description, amount)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
item_id,
|
|
||||||
invoice_id,
|
|
||||||
item.description,
|
|
||||||
int(item.amount * 100),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice_items = await get_invoice_items(invoice_id)
|
|
||||||
return invoice_items
|
|
||||||
|
|
||||||
|
|
||||||
async def update_invoice_internal(
|
|
||||||
wallet_id: str, data: Union[UpdateInvoiceData, Invoice]
|
|
||||||
) -> Invoice:
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE invoices.invoices
|
|
||||||
SET wallet = ?, currency = ?, status = ?, company_name = ?, first_name = ?, last_name = ?, email = ?, phone = ?, address = ?
|
|
||||||
WHERE id = ?
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
wallet_id,
|
|
||||||
data.currency,
|
|
||||||
data.status,
|
|
||||||
data.company_name,
|
|
||||||
data.first_name,
|
|
||||||
data.last_name,
|
|
||||||
data.email,
|
|
||||||
data.phone,
|
|
||||||
data.address,
|
|
||||||
data.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice = await get_invoice(data.id)
|
|
||||||
assert invoice, "Newly updated invoice couldn't be retrieved"
|
|
||||||
return invoice
|
|
||||||
|
|
||||||
|
|
||||||
async def update_invoice_items(
|
|
||||||
invoice_id: str, data: List[UpdateInvoiceItemData]
|
|
||||||
) -> List[InvoiceItem]:
|
|
||||||
updated_items = []
|
|
||||||
for item in data:
|
|
||||||
if item.id:
|
|
||||||
updated_items.append(item.id)
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE invoices.invoice_items
|
|
||||||
SET description = ?, amount = ?
|
|
||||||
WHERE id = ?
|
|
||||||
""",
|
|
||||||
(item.description, int(item.amount * 100), item.id),
|
|
||||||
)
|
|
||||||
|
|
||||||
placeholders = ",".join("?" for _ in range(len(updated_items)))
|
|
||||||
if not placeholders:
|
|
||||||
placeholders = "?"
|
|
||||||
updated_items = ["skip"]
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
DELETE FROM invoices.invoice_items
|
|
||||||
WHERE invoice_id = ?
|
|
||||||
AND id NOT IN ({placeholders})
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
invoice_id,
|
|
||||||
*tuple(updated_items),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for item in data:
|
|
||||||
if not item:
|
|
||||||
await create_invoice_items(
|
|
||||||
invoice_id=invoice_id,
|
|
||||||
data=[CreateInvoiceItemData(description=item.description)],
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice_items = await get_invoice_items(invoice_id)
|
|
||||||
return invoice_items
|
|
||||||
|
|
||||||
|
|
||||||
async def create_invoice_payment(invoice_id: str, amount: int) -> Payment:
|
|
||||||
payment_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO invoices.payments (id, invoice_id, amount)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
payment_id,
|
|
||||||
invoice_id,
|
|
||||||
amount,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
payment = await get_invoice_payment(payment_id)
|
|
||||||
assert payment, "Newly created payment couldn't be retrieved"
|
|
||||||
return payment
|
|
|
@ -1,55 +0,0 @@
|
||||||
async def m001_initial_invoices(db):
|
|
||||||
|
|
||||||
# STATUS COLUMN OPTIONS: 'draft', 'open', 'paid', 'canceled'
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE invoices.invoices (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
wallet TEXT NOT NULL,
|
|
||||||
|
|
||||||
status TEXT NOT NULL DEFAULT 'draft',
|
|
||||||
|
|
||||||
currency TEXT NOT NULL,
|
|
||||||
|
|
||||||
company_name TEXT DEFAULT NULL,
|
|
||||||
first_name TEXT DEFAULT NULL,
|
|
||||||
last_name TEXT DEFAULT NULL,
|
|
||||||
email TEXT DEFAULT NULL,
|
|
||||||
phone TEXT DEFAULT NULL,
|
|
||||||
address TEXT DEFAULT NULL,
|
|
||||||
|
|
||||||
|
|
||||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE invoices.invoice_items (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
invoice_id TEXT NOT NULL,
|
|
||||||
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
amount INTEGER NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE invoices.payments (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
invoice_id TEXT NOT NULL,
|
|
||||||
|
|
||||||
amount {db.big_int} NOT NULL,
|
|
||||||
|
|
||||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
|
||||||
|
|
||||||
FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
|
@ -1,104 +0,0 @@
|
||||||
from enum import Enum
|
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from fastapi import Query
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceStatusEnum(str, Enum):
|
|
||||||
draft = "draft"
|
|
||||||
open = "open"
|
|
||||||
paid = "paid"
|
|
||||||
canceled = "canceled"
|
|
||||||
|
|
||||||
|
|
||||||
class CreateInvoiceItemData(BaseModel):
|
|
||||||
description: str
|
|
||||||
amount: float = Query(..., ge=0.01)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateInvoiceData(BaseModel):
|
|
||||||
status: InvoiceStatusEnum = InvoiceStatusEnum.draft
|
|
||||||
currency: str
|
|
||||||
company_name: Optional[str]
|
|
||||||
first_name: Optional[str]
|
|
||||||
last_name: Optional[str]
|
|
||||||
email: Optional[str]
|
|
||||||
phone: Optional[str]
|
|
||||||
address: Optional[str]
|
|
||||||
items: List[CreateInvoiceItemData]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
use_enum_values = True
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateInvoiceItemData(BaseModel):
|
|
||||||
id: Optional[str]
|
|
||||||
description: str
|
|
||||||
amount: float = Query(..., ge=0.01)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateInvoiceData(BaseModel):
|
|
||||||
id: str
|
|
||||||
wallet: str
|
|
||||||
status: InvoiceStatusEnum = InvoiceStatusEnum.draft
|
|
||||||
currency: str
|
|
||||||
company_name: Optional[str]
|
|
||||||
first_name: Optional[str]
|
|
||||||
last_name: Optional[str]
|
|
||||||
email: Optional[str]
|
|
||||||
phone: Optional[str]
|
|
||||||
address: Optional[str]
|
|
||||||
items: List[UpdateInvoiceItemData]
|
|
||||||
|
|
||||||
|
|
||||||
class Invoice(BaseModel):
|
|
||||||
id: str
|
|
||||||
wallet: str
|
|
||||||
status: InvoiceStatusEnum = InvoiceStatusEnum.draft
|
|
||||||
currency: str
|
|
||||||
company_name: Optional[str]
|
|
||||||
first_name: Optional[str]
|
|
||||||
last_name: Optional[str]
|
|
||||||
email: Optional[str]
|
|
||||||
phone: Optional[str]
|
|
||||||
address: Optional[str]
|
|
||||||
time: int
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
use_enum_values = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "Invoice":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceItem(BaseModel):
|
|
||||||
id: str
|
|
||||||
invoice_id: str
|
|
||||||
description: str
|
|
||||||
amount: int
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "InvoiceItem":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
|
|
||||||
class Payment(BaseModel):
|
|
||||||
id: str
|
|
||||||
invoice_id: str
|
|
||||||
amount: int
|
|
||||||
time: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "Payment":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
|
|
||||||
class CreatePaymentData(BaseModel):
|
|
||||||
invoice_id: str
|
|
||||||
amount: int
|
|
|
@ -1,67 +0,0 @@
|
||||||
#invoicePage > .row:first-child > .col-md-6 {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#invoicePage > .row:first-child > .col-md-6 > .q-card {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#invoicePage .clear {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#printQrCode {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
#invoicePage > .row:first-child > .col-md-6:first-child > div {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#invoicePage > .row:first-child > .col-md-6:nth-child(2) > div {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
* {
|
|
||||||
color: black !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
header,
|
|
||||||
button,
|
|
||||||
#payButtonContainer {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
main,
|
|
||||||
.q-page-container {
|
|
||||||
padding-top: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-card {
|
|
||||||
box-shadow: none !important;
|
|
||||||
border: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-item {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-card__section {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#printQrCode {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#invoicePage .clear {
|
|
||||||
margin-bottom: 10px !important;
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 8.6 KiB |
|
@ -1,52 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
|
||||||
|
|
||||||
from .crud import (
|
|
||||||
create_invoice_payment,
|
|
||||||
get_invoice,
|
|
||||||
get_invoice_items,
|
|
||||||
get_invoice_payments,
|
|
||||||
get_invoice_total,
|
|
||||||
get_payments_total,
|
|
||||||
update_invoice_internal,
|
|
||||||
)
|
|
||||||
from .models import InvoiceStatusEnum
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
|
||||||
invoice_queue = asyncio.Queue()
|
|
||||||
register_invoice_listener(invoice_queue)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
payment = await invoice_queue.get()
|
|
||||||
await on_invoice_paid(payment)
|
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
|
||||||
if payment.extra.get("tag") != "invoices":
|
|
||||||
return
|
|
||||||
|
|
||||||
invoice_id = payment.extra.get("invoice_id")
|
|
||||||
assert invoice_id
|
|
||||||
|
|
||||||
amount = payment.extra.get("famount")
|
|
||||||
assert amount
|
|
||||||
|
|
||||||
await create_invoice_payment(invoice_id=invoice_id, amount=amount)
|
|
||||||
|
|
||||||
invoice = await get_invoice(invoice_id)
|
|
||||||
assert invoice
|
|
||||||
|
|
||||||
invoice_items = await get_invoice_items(invoice_id)
|
|
||||||
invoice_total = await get_invoice_total(invoice_items)
|
|
||||||
|
|
||||||
invoice_payments = await get_invoice_payments(invoice_id)
|
|
||||||
payments_total = await get_payments_total(invoice_payments)
|
|
||||||
|
|
||||||
if payments_total >= invoice_total:
|
|
||||||
invoice.status = InvoiceStatusEnum.paid
|
|
||||||
await update_invoice_internal(invoice.wallet, invoice)
|
|
||||||
|
|
||||||
return
|
|
|
@ -1,153 +0,0 @@
|
||||||
<q-expansion-item
|
|
||||||
group="extras"
|
|
||||||
icon="swap_vertical_circle"
|
|
||||||
label="API info"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="List Invoices">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span> /invoices/api/v1/invoices</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>[<invoice_object>, ...]</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url }}invoices/api/v1/invoices -H
|
|
||||||
"X-Api-Key: <invoice_key>"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Fetch Invoice">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span>
|
|
||||||
/invoices/api/v1/invoice/{invoice_id}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{invoice_object}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url
|
|
||||||
}}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
|
|
||||||
<invoice_key>"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Create Invoice">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">POST</span> /invoices/api/v1/invoice</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{invoice_object}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST {{ request.base_url }}invoices/api/v1/invoice -H
|
|
||||||
"X-Api-Key: <invoice_key>"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Update Invoice">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">POST</span>
|
|
||||||
/invoices/api/v1/invoice/{invoice_id}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{invoice_object}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST {{ request.base_url
|
|
||||||
}}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
|
|
||||||
<invoice_key>"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Create Invoice Payment"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">POST</span>
|
|
||||||
/invoices/api/v1/invoice/{invoice_id}/payments</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{payment_object}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST {{ request.base_url
|
|
||||||
}}invoices/api/v1/invoice/{invoice_id}/payments -H "X-Api-Key:
|
|
||||||
<invoice_key>"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Check Invoice Payment Status"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span>
|
|
||||||
/invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url
|
|
||||||
}}invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash} -H
|
|
||||||
"X-Api-Key: <invoice_key>"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-expansion-item>
|
|
|
@ -1,571 +0,0 @@
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
|
||||||
%} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
|
||||||
>New Invoice</q-btn
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col">
|
|
||||||
<h5 class="text-subtitle1 q-my-none">Invoices</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:data="invoices"
|
|
||||||
row-key="id"
|
|
||||||
:columns="invoicesTable.columns"
|
|
||||||
:pagination.sync="invoicesTable.pagination"
|
|
||||||
>
|
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.label }}
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="edit"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
@click="showEditModal(props.row)"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="launch"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
type="a"
|
|
||||||
:href="'pay/' + props.row.id"
|
|
||||||
target="_blank"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.value }}
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h6 class="text-subtitle1 q-my-none">
|
|
||||||
{{SITE_TITLE}} Invoices extension
|
|
||||||
</h6>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-list> {% include "invoices/_api_docs.html" %} </q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="saveInvoice" class="q-gutter-md">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="formDialog.data.wallet"
|
|
||||||
:options="g.user.walletOptions"
|
|
||||||
label="Wallet *"
|
|
||||||
></q-select>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="formDialog.data.currency"
|
|
||||||
:options="currencyOptions"
|
|
||||||
label="Currency *"
|
|
||||||
></q-select>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="formDialog.data.status"
|
|
||||||
:options="['draft', 'open', 'paid', 'canceled']"
|
|
||||||
label="Status *"
|
|
||||||
></q-select>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.company_name"
|
|
||||||
label="Company Name"
|
|
||||||
placeholder="LNbits Labs"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.first_name"
|
|
||||||
label="First Name"
|
|
||||||
placeholder="Satoshi"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.last_name"
|
|
||||||
label="Last Name"
|
|
||||||
placeholder="Nakamoto"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.email"
|
|
||||||
label="Email"
|
|
||||||
placeholder="satoshi@gmail.com"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.phone"
|
|
||||||
label="Phone"
|
|
||||||
placeholder="+81 (012)-345-6789"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.address"
|
|
||||||
label="Address"
|
|
||||||
placeholder="1600 Pennsylvania Ave."
|
|
||||||
type="textarea"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<q-list bordered separator>
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
v-ripple
|
|
||||||
v-for="(item, index) in formDialog.invoiceItems"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Item"
|
|
||||||
placeholder="Jelly Beans"
|
|
||||||
v-model="formDialog.invoiceItems[index].description"
|
|
||||||
></q-input>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Amount"
|
|
||||||
placeholder="4.20"
|
|
||||||
v-model="formDialog.invoiceItems[index].amount"
|
|
||||||
></q-input>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="delete"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
@click="formDialog.invoiceItems.splice(index, 1)"
|
|
||||||
></q-btn>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-btn flat icon="add" @click="formDialog.invoiceItems.push({})">
|
|
||||||
Add Line Item
|
|
||||||
</q-btn>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="formDialog.data.wallet == null || formDialog.data.currency == null"
|
|
||||||
type="submit"
|
|
||||||
v-if="typeof formDialog.data.id == 'undefined'"
|
|
||||||
>Create Invoice</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="formDialog.data.wallet == null || formDialog.data.currency == null"
|
|
||||||
type="submit"
|
|
||||||
v-if="typeof formDialog.data.id !== 'undefined'"
|
|
||||||
>Save Invoice</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
|
||||||
<script>
|
|
||||||
var mapInvoice = function (obj) {
|
|
||||||
obj.time = Quasar.utils.date.formatDate(
|
|
||||||
new Date(obj.time * 1000),
|
|
||||||
'YYYY-MM-DD HH:mm'
|
|
||||||
)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
var mapInvoiceItems = function (obj) {
|
|
||||||
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
invoices: [],
|
|
||||||
currencyOptions: [
|
|
||||||
'USD',
|
|
||||||
'EUR',
|
|
||||||
'GBP',
|
|
||||||
'AED',
|
|
||||||
'AFN',
|
|
||||||
'ALL',
|
|
||||||
'AMD',
|
|
||||||
'ANG',
|
|
||||||
'AOA',
|
|
||||||
'ARS',
|
|
||||||
'AUD',
|
|
||||||
'AWG',
|
|
||||||
'AZN',
|
|
||||||
'BAM',
|
|
||||||
'BBD',
|
|
||||||
'BDT',
|
|
||||||
'BGN',
|
|
||||||
'BHD',
|
|
||||||
'BIF',
|
|
||||||
'BMD',
|
|
||||||
'BND',
|
|
||||||
'BOB',
|
|
||||||
'BRL',
|
|
||||||
'BSD',
|
|
||||||
'BTN',
|
|
||||||
'BWP',
|
|
||||||
'BYN',
|
|
||||||
'BZD',
|
|
||||||
'CAD',
|
|
||||||
'CDF',
|
|
||||||
'CHF',
|
|
||||||
'CLF',
|
|
||||||
'CLP',
|
|
||||||
'CNH',
|
|
||||||
'CNY',
|
|
||||||
'COP',
|
|
||||||
'CRC',
|
|
||||||
'CUC',
|
|
||||||
'CUP',
|
|
||||||
'CVE',
|
|
||||||
'CZK',
|
|
||||||
'DJF',
|
|
||||||
'DKK',
|
|
||||||
'DOP',
|
|
||||||
'DZD',
|
|
||||||
'EGP',
|
|
||||||
'ERN',
|
|
||||||
'ETB',
|
|
||||||
'EUR',
|
|
||||||
'FJD',
|
|
||||||
'FKP',
|
|
||||||
'GBP',
|
|
||||||
'GEL',
|
|
||||||
'GGP',
|
|
||||||
'GHS',
|
|
||||||
'GIP',
|
|
||||||
'GMD',
|
|
||||||
'GNF',
|
|
||||||
'GTQ',
|
|
||||||
'GYD',
|
|
||||||
'HKD',
|
|
||||||
'HNL',
|
|
||||||
'HRK',
|
|
||||||
'HTG',
|
|
||||||
'HUF',
|
|
||||||
'IDR',
|
|
||||||
'ILS',
|
|
||||||
'IMP',
|
|
||||||
'INR',
|
|
||||||
'IQD',
|
|
||||||
'IRR',
|
|
||||||
'IRT',
|
|
||||||
'ISK',
|
|
||||||
'JEP',
|
|
||||||
'JMD',
|
|
||||||
'JOD',
|
|
||||||
'JPY',
|
|
||||||
'KES',
|
|
||||||
'KGS',
|
|
||||||
'KHR',
|
|
||||||
'KMF',
|
|
||||||
'KPW',
|
|
||||||
'KRW',
|
|
||||||
'KWD',
|
|
||||||
'KYD',
|
|
||||||
'KZT',
|
|
||||||
'LAK',
|
|
||||||
'LBP',
|
|
||||||
'LKR',
|
|
||||||
'LRD',
|
|
||||||
'LSL',
|
|
||||||
'LYD',
|
|
||||||
'MAD',
|
|
||||||
'MDL',
|
|
||||||
'MGA',
|
|
||||||
'MKD',
|
|
||||||
'MMK',
|
|
||||||
'MNT',
|
|
||||||
'MOP',
|
|
||||||
'MRO',
|
|
||||||
'MUR',
|
|
||||||
'MVR',
|
|
||||||
'MWK',
|
|
||||||
'MXN',
|
|
||||||
'MYR',
|
|
||||||
'MZN',
|
|
||||||
'NAD',
|
|
||||||
'NGN',
|
|
||||||
'NIO',
|
|
||||||
'NOK',
|
|
||||||
'NPR',
|
|
||||||
'NZD',
|
|
||||||
'OMR',
|
|
||||||
'PAB',
|
|
||||||
'PEN',
|
|
||||||
'PGK',
|
|
||||||
'PHP',
|
|
||||||
'PKR',
|
|
||||||
'PLN',
|
|
||||||
'PYG',
|
|
||||||
'QAR',
|
|
||||||
'RON',
|
|
||||||
'RSD',
|
|
||||||
'RUB',
|
|
||||||
'RWF',
|
|
||||||
'SAR',
|
|
||||||
'SBD',
|
|
||||||
'SCR',
|
|
||||||
'SDG',
|
|
||||||
'SEK',
|
|
||||||
'SGD',
|
|
||||||
'SHP',
|
|
||||||
'SLL',
|
|
||||||
'SOS',
|
|
||||||
'SRD',
|
|
||||||
'SSP',
|
|
||||||
'STD',
|
|
||||||
'SVC',
|
|
||||||
'SYP',
|
|
||||||
'SZL',
|
|
||||||
'THB',
|
|
||||||
'TJS',
|
|
||||||
'TMT',
|
|
||||||
'TND',
|
|
||||||
'TOP',
|
|
||||||
'TRY',
|
|
||||||
'TTD',
|
|
||||||
'TWD',
|
|
||||||
'TZS',
|
|
||||||
'UAH',
|
|
||||||
'UGX',
|
|
||||||
'USD',
|
|
||||||
'UYU',
|
|
||||||
'UZS',
|
|
||||||
'VEF',
|
|
||||||
'VES',
|
|
||||||
'VND',
|
|
||||||
'VUV',
|
|
||||||
'WST',
|
|
||||||
'XAF',
|
|
||||||
'XAG',
|
|
||||||
'XAU',
|
|
||||||
'XCD',
|
|
||||||
'XDR',
|
|
||||||
'XOF',
|
|
||||||
'XPD',
|
|
||||||
'XPF',
|
|
||||||
'XPT',
|
|
||||||
'YER',
|
|
||||||
'ZAR',
|
|
||||||
'ZMW',
|
|
||||||
'ZWL'
|
|
||||||
],
|
|
||||||
invoicesTable: {
|
|
||||||
columns: [
|
|
||||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
|
||||||
{name: 'status', align: 'left', label: 'Status', field: 'status'},
|
|
||||||
{name: 'time', align: 'left', label: 'Created', field: 'time'},
|
|
||||||
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
|
|
||||||
{
|
|
||||||
name: 'currency',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Currency',
|
|
||||||
field: 'currency'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'company_name',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Company Name',
|
|
||||||
field: 'company_name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'first_name',
|
|
||||||
align: 'left',
|
|
||||||
label: 'First Name',
|
|
||||||
field: 'first_name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'last_name',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Last Name',
|
|
||||||
field: 'last_name'
|
|
||||||
},
|
|
||||||
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
|
||||||
{name: 'phone', align: 'left', label: 'Phone', field: 'phone'},
|
|
||||||
{name: 'address', align: 'left', label: 'Address', field: 'address'}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {},
|
|
||||||
invoiceItems: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
closeFormDialog: function () {
|
|
||||||
this.formDialog.data = {}
|
|
||||||
this.formDialog.invoiceItems = []
|
|
||||||
},
|
|
||||||
showEditModal: function (obj) {
|
|
||||||
this.formDialog.data = obj
|
|
||||||
this.formDialog.show = true
|
|
||||||
|
|
||||||
this.getInvoice(obj.id)
|
|
||||||
},
|
|
||||||
getInvoice: function (invoice_id) {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/invoices/api/v1/invoice/' + invoice_id)
|
|
||||||
.then(function (response) {
|
|
||||||
self.formDialog.invoiceItems = response.data.items.map(function (
|
|
||||||
obj
|
|
||||||
) {
|
|
||||||
return mapInvoiceItems(obj)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getInvoices: function () {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/invoices/api/v1/invoices?all_wallets=true',
|
|
||||||
this.g.user.wallets[0].inkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.invoices = response.data.map(function (obj) {
|
|
||||||
return mapInvoice(obj)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
saveInvoice: function () {
|
|
||||||
var data = this.formDialog.data
|
|
||||||
data.items = this.formDialog.invoiceItems
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'POST',
|
|
||||||
'/invoices/api/v1/invoice' + (data.id ? '/' + data.id : ''),
|
|
||||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
|
|
||||||
.inkey,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
if (!data.id) {
|
|
||||||
self.invoices.push(mapInvoice(response.data))
|
|
||||||
} else {
|
|
||||||
self.getInvoices()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.formDialog.invoiceItems = []
|
|
||||||
self.formDialog.show = false
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteTPoS: function (tposId) {
|
|
||||||
var self = this
|
|
||||||
var tpos = _.findWhere(this.tposs, {id: tposId})
|
|
||||||
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this TPoS?')
|
|
||||||
.onOk(function () {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'DELETE',
|
|
||||||
'/tpos/api/v1/tposs/' + tposId,
|
|
||||||
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.tposs = _.reject(self.tposs, function (obj) {
|
|
||||||
return obj.id == tposId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
exportCSV: function () {
|
|
||||||
LNbits.utils.exportCSV(this.invoicesTable.columns, this.invoices)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: function () {
|
|
||||||
if (this.g.user.wallets.length) {
|
|
||||||
this.getInvoices()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,433 +0,0 @@
|
||||||
{% extends "public.html" %} {% block toolbar_title %} Invoice
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="md"
|
|
||||||
@click.prevent="urlDialog.show = true"
|
|
||||||
icon="share"
|
|
||||||
color="white"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="md"
|
|
||||||
@click.prevent="printInvoice()"
|
|
||||||
icon="print"
|
|
||||||
color="white"
|
|
||||||
></q-btn>
|
|
||||||
{% endblock %} {% from "macros.jinja" import window_vars with context %} {%
|
|
||||||
block page %}
|
|
||||||
<link rel="stylesheet" href="/invoices/static/css/pay.css" />
|
|
||||||
<div id="invoicePage">
|
|
||||||
<div class="row q-gutter-y-md">
|
|
||||||
<div class="col-md-6 col-sm-12 col-xs-12">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<p>
|
|
||||||
<b>Invoice</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<q-list bordered separator>
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>ID</b></q-item-section>
|
|
||||||
<q-item-section style="word-break: break-all"
|
|
||||||
>{{ invoice_id }}</q-item-section
|
|
||||||
>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Created At</b></q-item-section>
|
|
||||||
<q-item-section
|
|
||||||
>{{ datetime.utcfromtimestamp(invoice.time).strftime('%Y-%m-%d
|
|
||||||
%H:%M') }}</q-item-section
|
|
||||||
>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Status</b></q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<span>
|
|
||||||
<q-badge color=""> {{ invoice.status }} </q-badge>
|
|
||||||
</span>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Total</b></q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
{{ "{:0,.2f}".format(invoice_total / 100) }} {{ invoice.currency
|
|
||||||
}}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Paid</b></q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<div class="row" style="align-items: center">
|
|
||||||
<div class="col-sm-6">
|
|
||||||
{{ "{:0,.2f}".format(payments_total / 100) }} {{
|
|
||||||
invoice.currency }}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6" id="payButtonContainer">
|
|
||||||
{% if payments_total < invoice_total %}
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
@click="formDialog.show = true"
|
|
||||||
v-if="status == 'open'"
|
|
||||||
>
|
|
||||||
Pay Invoice
|
|
||||||
</q-btn>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12 col-xs-12">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<p>
|
|
||||||
<b>Bill To</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<q-list bordered separator>
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Company Name</b></q-item-section>
|
|
||||||
<q-item-section>{{ invoice.company_name }}</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Name</b></q-item-section>
|
|
||||||
<q-item-section
|
|
||||||
>{{ invoice.first_name }} {{ invoice.last_name
|
|
||||||
}}</q-item-section
|
|
||||||
>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Address</b></q-item-section>
|
|
||||||
<q-item-section>{{ invoice.address }}</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Email</b></q-item-section>
|
|
||||||
<q-item-section>{{ invoice.email }}</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Phone</b></q-item-section>
|
|
||||||
<q-item-section>{{ invoice.phone }}</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<p>
|
|
||||||
<b>Items</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<q-list bordered separator>
|
|
||||||
{% if invoice_items %}
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Item</b></q-item-section>
|
|
||||||
<q-item-section side><b>Amount</b></q-item-section>
|
|
||||||
</q-item>
|
|
||||||
{% endif %} {% for item in invoice_items %}
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>{{item.description}}</b></q-item-section>
|
|
||||||
<q-item-section side>
|
|
||||||
{{ "{:0,.2f}".format(item.amount / 100) }} {{ invoice.currency
|
|
||||||
}}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
{% endfor %} {% if not invoice_items %} No Invoice Items {% endif %}
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<p>
|
|
||||||
<b>Payments</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<q-list bordered separator>
|
|
||||||
{% if invoice_payments %}
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section><b>Date</b></q-item-section>
|
|
||||||
<q-item-section side><b>Amount</b></q-item-section>
|
|
||||||
</q-item>
|
|
||||||
{% endif %} {% for item in invoice_payments %}
|
|
||||||
<q-item clickable v-ripple>
|
|
||||||
<q-item-section
|
|
||||||
><b
|
|
||||||
>{{ datetime.utcfromtimestamp(item.time).strftime('%Y-%m-%d
|
|
||||||
%H:%M') }}</b
|
|
||||||
></q-item-section
|
|
||||||
>
|
|
||||||
<q-item-section side>
|
|
||||||
{{ "{:0,.2f}".format(item.amount / 100) }} {{ invoice.currency
|
|
||||||
}}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
{% endfor %} {% if not invoice_payments %} No Invoice Payments {%
|
|
||||||
endif %}
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
|
|
||||||
<div class="row q-gutter-y-md q-gutter-md" id="printQrCode">
|
|
||||||
<div class="col-12 col-md">
|
|
||||||
<div class="text-center">
|
|
||||||
<p><b>Scan to View & Pay Online!</b></p>
|
|
||||||
<qrcode
|
|
||||||
value="{{ request.url }}"
|
|
||||||
:options="{width: 200}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form @submit="createPayment" class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.payment_amount"
|
|
||||||
:rules="[val => val >= 0.01 || 'Minimum amount is 0.01']"
|
|
||||||
min="0.01"
|
|
||||||
label="Payment Amount"
|
|
||||||
placeholder="4.20"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<span style="font-size: 12px"> {{ invoice.currency }} </span>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="formDialog.data.payment_amount == null"
|
|
||||||
type="submit"
|
|
||||||
>Create Payment</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="qrCodeDialog.show"
|
|
||||||
position="top"
|
|
||||||
@hide="closeQrCodeDialog"
|
|
||||||
>
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card text-center">
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
:href="'lightning:' + qrCodeDialog.data.payment_request"
|
|
||||||
>
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xs">
|
|
||||||
<qrcode
|
|
||||||
:value="'lightning:' + qrCodeDialog.data.payment_request"
|
|
||||||
:options="{width: 400}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
<br />
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText('lightning:' + qrCodeDialog.data.payment_request, 'Invoice copied to clipboard!')"
|
|
||||||
>Copy Invoice</q-btn
|
|
||||||
>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="urlDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
|
||||||
<qrcode
|
|
||||||
value="{{ request.url }}"
|
|
||||||
:options="{width: 400}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
<div class="text-center q-mb-xl">
|
|
||||||
<p style="word-break: break-all">{{ request.url }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText('{{ request.url }}', 'Invoice Pay URL copied to clipboard!')"
|
|
||||||
>Copy URL</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
var mapInvoice = function (obj) {
|
|
||||||
obj.time = Quasar.utils.date.formatDate(
|
|
||||||
new Date(obj.time * 1000),
|
|
||||||
'YYYY-MM-DD HH:mm'
|
|
||||||
)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
var mapInvoiceItems = function (obj) {
|
|
||||||
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
invoice_id: '{{ invoice.id }}',
|
|
||||||
wallet: '{{ invoice.wallet }}',
|
|
||||||
currency: '{{ invoice.currency }}',
|
|
||||||
status: '{{ invoice.status }}',
|
|
||||||
qrCodeDialog: {
|
|
||||||
data: {
|
|
||||||
payment_request: null,
|
|
||||||
},
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
formDialog: {
|
|
||||||
data: {
|
|
||||||
payment_amount: parseFloat({{invoice_total - payments_total}} / 100).toFixed(2)
|
|
||||||
},
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
urlDialog: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
printInvoice: function() {
|
|
||||||
window.print()
|
|
||||||
},
|
|
||||||
closeFormDialog: function() {
|
|
||||||
this.formDialog.show = false
|
|
||||||
},
|
|
||||||
closeQrCodeDialog: function() {
|
|
||||||
this.qrCodeDialog.show = false
|
|
||||||
},
|
|
||||||
createPayment: function () {
|
|
||||||
var self = this
|
|
||||||
var qrCodeDialog = this.qrCodeDialog
|
|
||||||
var formDialog = this.formDialog
|
|
||||||
var famount = parseInt(formDialog.data.payment_amount * 100)
|
|
||||||
|
|
||||||
axios
|
|
||||||
.post('/invoices/api/v1/invoice/' + this.invoice_id + '/payments', null, {
|
|
||||||
params: {
|
|
||||||
famount: famount,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
|
||||||
formDialog.show = false
|
|
||||||
formDialog.data = {}
|
|
||||||
|
|
||||||
qrCodeDialog.data = response.data
|
|
||||||
qrCodeDialog.show = true
|
|
||||||
|
|
||||||
console.log(qrCodeDialog.data)
|
|
||||||
|
|
||||||
qrCodeDialog.dismissMsg = self.$q.notify({
|
|
||||||
timeout: 0,
|
|
||||||
message: 'Waiting for payment...'
|
|
||||||
})
|
|
||||||
|
|
||||||
qrCodeDialog.paymentChecker = setInterval(function () {
|
|
||||||
axios
|
|
||||||
.get(
|
|
||||||
'/invoices/api/v1/invoice/' +
|
|
||||||
self.invoice_id +
|
|
||||||
'/payments/' +
|
|
||||||
response.data.payment_hash
|
|
||||||
)
|
|
||||||
.then(function (res) {
|
|
||||||
if (res.data.paid) {
|
|
||||||
clearInterval(qrCodeDialog.paymentChecker)
|
|
||||||
qrCodeDialog.dismissMsg()
|
|
||||||
qrCodeDialog.show = false
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.reload()
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
statusBadgeColor: function() {
|
|
||||||
switch(this.status) {
|
|
||||||
case 'draft':
|
|
||||||
return 'gray'
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'open':
|
|
||||||
return 'blue'
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'paid':
|
|
||||||
return 'green'
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'canceled':
|
|
||||||
return 'red'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
created: function () {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,57 +0,0 @@
|
||||||
from datetime import datetime
|
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Request
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.models import User
|
|
||||||
from lnbits.decorators import check_user_exists
|
|
||||||
|
|
||||||
from . import invoices_ext, invoices_renderer
|
|
||||||
from .crud import (
|
|
||||||
get_invoice,
|
|
||||||
get_invoice_items,
|
|
||||||
get_invoice_payments,
|
|
||||||
get_invoice_total,
|
|
||||||
get_payments_total,
|
|
||||||
)
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.get("/", response_class=HTMLResponse)
|
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
return invoices_renderer().TemplateResponse(
|
|
||||||
"invoices/index.html", {"request": request, "user": user.dict()}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.get("/pay/{invoice_id}", response_class=HTMLResponse)
|
|
||||||
async def pay(request: Request, invoice_id: str):
|
|
||||||
invoice = await get_invoice(invoice_id)
|
|
||||||
|
|
||||||
if not invoice:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice_items = await get_invoice_items(invoice_id)
|
|
||||||
invoice_total = await get_invoice_total(invoice_items)
|
|
||||||
|
|
||||||
invoice_payments = await get_invoice_payments(invoice_id)
|
|
||||||
payments_total = await get_payments_total(invoice_payments)
|
|
||||||
|
|
||||||
return invoices_renderer().TemplateResponse(
|
|
||||||
"invoices/pay.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"invoice_id": invoice_id,
|
|
||||||
"invoice": invoice.dict(),
|
|
||||||
"invoice_items": invoice_items,
|
|
||||||
"invoice_total": invoice_total,
|
|
||||||
"invoice_payments": invoice_payments,
|
|
||||||
"payments_total": payments_total,
|
|
||||||
"datetime": datetime,
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,133 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Query
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
|
||||||
from lnbits.core.services import create_invoice
|
|
||||||
from lnbits.core.views.api import api_payment
|
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
|
||||||
|
|
||||||
from . import invoices_ext
|
|
||||||
from .crud import (
|
|
||||||
create_invoice_internal,
|
|
||||||
create_invoice_items,
|
|
||||||
get_invoice,
|
|
||||||
get_invoice_items,
|
|
||||||
get_invoice_payments,
|
|
||||||
get_invoice_total,
|
|
||||||
get_invoices,
|
|
||||||
get_payments_total,
|
|
||||||
update_invoice_internal,
|
|
||||||
update_invoice_items,
|
|
||||||
)
|
|
||||||
from .models import CreateInvoiceData, UpdateInvoiceData
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.get("/api/v1/invoices", status_code=HTTPStatus.OK)
|
|
||||||
async def api_invoices(
|
|
||||||
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
):
|
|
||||||
wallet_ids = [wallet.wallet.id]
|
|
||||||
if all_wallets:
|
|
||||||
user = await get_user(wallet.wallet.user)
|
|
||||||
wallet_ids = user.wallet_ids if user else []
|
|
||||||
|
|
||||||
return [invoice.dict() for invoice in await get_invoices(wallet_ids)]
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.get("/api/v1/invoice/{invoice_id}", status_code=HTTPStatus.OK)
|
|
||||||
async def api_invoice(invoice_id: str):
|
|
||||||
invoice = await get_invoice(invoice_id)
|
|
||||||
if not invoice:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
|
|
||||||
)
|
|
||||||
invoice_items = await get_invoice_items(invoice_id)
|
|
||||||
|
|
||||||
invoice_payments = await get_invoice_payments(invoice_id)
|
|
||||||
payments_total = await get_payments_total(invoice_payments)
|
|
||||||
|
|
||||||
invoice_dict = invoice.dict()
|
|
||||||
invoice_dict["items"] = invoice_items
|
|
||||||
invoice_dict["payments"] = payments_total
|
|
||||||
return invoice_dict
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.post("/api/v1/invoice", status_code=HTTPStatus.CREATED)
|
|
||||||
async def api_invoice_create(
|
|
||||||
data: CreateInvoiceData, wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
):
|
|
||||||
invoice = await create_invoice_internal(wallet_id=wallet.wallet.id, data=data)
|
|
||||||
items = await create_invoice_items(invoice_id=invoice.id, data=data.items)
|
|
||||||
invoice_dict = invoice.dict()
|
|
||||||
invoice_dict["items"] = items
|
|
||||||
return invoice_dict
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.post("/api/v1/invoice/{invoice_id}", status_code=HTTPStatus.OK)
|
|
||||||
async def api_invoice_update(
|
|
||||||
data: UpdateInvoiceData,
|
|
||||||
invoice_id: str,
|
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
|
||||||
):
|
|
||||||
invoice = await update_invoice_internal(wallet_id=wallet.wallet.id, data=data)
|
|
||||||
items = await update_invoice_items(invoice_id=invoice.id, data=data.items)
|
|
||||||
invoice_dict = invoice.dict()
|
|
||||||
invoice_dict["items"] = items
|
|
||||||
return invoice_dict
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.post(
|
|
||||||
"/api/v1/invoice/{invoice_id}/payments", status_code=HTTPStatus.CREATED
|
|
||||||
)
|
|
||||||
async def api_invoices_create_payment(invoice_id: str, famount: int = Query(..., ge=1)):
|
|
||||||
invoice = await get_invoice(invoice_id)
|
|
||||||
invoice_items = await get_invoice_items(invoice_id)
|
|
||||||
invoice_total = await get_invoice_total(invoice_items)
|
|
||||||
|
|
||||||
invoice_payments = await get_invoice_payments(invoice_id)
|
|
||||||
payments_total = await get_payments_total(invoice_payments)
|
|
||||||
|
|
||||||
if payments_total + famount > invoice_total:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Amount exceeds invoice due."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not invoice:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
price_in_sats = await fiat_amount_as_satoshis(famount / 100, invoice.currency)
|
|
||||||
|
|
||||||
try:
|
|
||||||
payment_hash, payment_request = await create_invoice(
|
|
||||||
wallet_id=invoice.wallet,
|
|
||||||
amount=price_in_sats,
|
|
||||||
memo=f"Payment for invoice {invoice_id}",
|
|
||||||
extra={"tag": "invoices", "invoice_id": invoice_id, "famount": famount},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
|
||||||
|
|
||||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.get(
|
|
||||||
"/api/v1/invoice/{invoice_id}/payments/{payment_hash}", status_code=HTTPStatus.OK
|
|
||||||
)
|
|
||||||
async def api_invoices_check_payment(invoice_id: str, payment_hash: str):
|
|
||||||
invoice = await get_invoice(invoice_id)
|
|
||||||
if not invoice:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
status = await api_payment(payment_hash)
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error(exc)
|
|
||||||
return {"paid": False}
|
|
||||||
return status
|
|
|
@ -1,8 +1,63 @@
|
||||||
$themes: ( 'classic': ( primary: #673ab7, secondary: #9c27b0, dark: #1f2234, info: #333646, marginal-bg: #1f2234, marginal-text: #fff), 'bitcoin': ( primary: #ff9853, secondary: #ff7353, dark: #2d293b, info: #333646, marginal-bg: #2d293b, marginal-text: #fff), 'freedom': ( primary: #e22156, secondary: #b91a45, dark: #0a0a0a, info: #1b1b1b, marginal-bg: #2d293b, marginal-text: #fff), 'mint': ( primary: #3ab77d, secondary: #27b065, dark: #1f342b, info: #334642, marginal-bg: #1f342b, marginal-text: #fff), 'autumn': ( primary: #b7763a, secondary: #b07927, dark: #34291f, info: #463f33, marginal-bg: #342a1f, marginal-text: rgb(255, 255, 255)), 'flamingo': ( primary: #d11d53, secondary: #db3e6d, dark: #803a45, info: #ec7599, marginal-bg: #803a45, marginal-text: rgb(255, 255, 255)), 'monochrome': ( primary: #494949, secondary: #6b6b6b, dark: #000, info: rgb(39, 39, 39), marginal-bg: #000, marginal-text: rgb(255, 255, 255)));
|
$themes: (
|
||||||
@each $theme,
|
'classic': (
|
||||||
$colors in $themes {
|
primary: #673ab7,
|
||||||
@each $name,
|
secondary: #9c27b0,
|
||||||
$color in $colors {
|
dark: #1f2234,
|
||||||
|
info: #333646,
|
||||||
|
marginal-bg: #1f2234,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'bitcoin': (
|
||||||
|
primary: #ff9853,
|
||||||
|
secondary: #ff7353,
|
||||||
|
dark: #2d293b,
|
||||||
|
info: #333646,
|
||||||
|
marginal-bg: #2d293b,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'freedom': (
|
||||||
|
primary: #e22156,
|
||||||
|
secondary: #b91a45,
|
||||||
|
dark: #0a0a0a,
|
||||||
|
info: #1b1b1b,
|
||||||
|
marginal-bg: #2d293b,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'mint': (
|
||||||
|
primary: #3ab77d,
|
||||||
|
secondary: #27b065,
|
||||||
|
dark: #1f342b,
|
||||||
|
info: #334642,
|
||||||
|
marginal-bg: #1f342b,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'autumn': (
|
||||||
|
primary: #b7763a,
|
||||||
|
secondary: #b07927,
|
||||||
|
dark: #34291f,
|
||||||
|
info: #463f33,
|
||||||
|
marginal-bg: #342a1f,
|
||||||
|
marginal-text: rgb(255, 255, 255)
|
||||||
|
),
|
||||||
|
'flamingo': (
|
||||||
|
primary: #d11d53,
|
||||||
|
secondary: #db3e6d,
|
||||||
|
dark: #803a45,
|
||||||
|
info: #ec7599,
|
||||||
|
marginal-bg: #803a45,
|
||||||
|
marginal-text: rgb(255, 255, 255)
|
||||||
|
),
|
||||||
|
'monochrome': (
|
||||||
|
primary: #494949,
|
||||||
|
secondary: #6b6b6b,
|
||||||
|
dark: #000,
|
||||||
|
info: rgb(39, 39, 39),
|
||||||
|
marginal-bg: #000,
|
||||||
|
marginal-text: rgb(255, 255, 255)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
@each $theme, $colors in $themes {
|
||||||
|
@each $name, $color in $colors {
|
||||||
@if $name== 'dark' {
|
@if $name== 'dark' {
|
||||||
[data-theme='#{$theme}'] .q-drawer--dark,
|
[data-theme='#{$theme}'] .q-drawer--dark,
|
||||||
body[data-theme='#{$theme}'].body--dark,
|
body[data-theme='#{$theme}'].body--dark,
|
||||||
|
@ -24,8 +79,7 @@ $colors in $themes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[data-theme='#{$theme}'] {
|
[data-theme='#{$theme}'] {
|
||||||
@each $name,
|
@each $name, $color in $colors {
|
||||||
$color in $colors {
|
|
||||||
.bg-#{$name} {
|
.bg-#{$name} {
|
||||||
background: $color !important;
|
background: $color !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from tests.mocks import WALLET # noqa: F401
|
|
@ -1,36 +0,0 @@
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from lnbits.core.crud import create_account, create_wallet
|
|
||||||
from lnbits.extensions.invoices.crud import (
|
|
||||||
create_invoice_internal,
|
|
||||||
create_invoice_items,
|
|
||||||
)
|
|
||||||
from lnbits.extensions.invoices.models import CreateInvoiceData
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
|
||||||
async def invoices_wallet():
|
|
||||||
user = await create_account()
|
|
||||||
wallet = await create_wallet(user_id=user.id, wallet_name="invoices_test")
|
|
||||||
|
|
||||||
return wallet
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
|
||||||
async def accounting_invoice(invoices_wallet):
|
|
||||||
invoice_data = CreateInvoiceData(
|
|
||||||
status="open",
|
|
||||||
currency="USD",
|
|
||||||
company_name="LNbits, Inc",
|
|
||||||
first_name="Ben",
|
|
||||||
last_name="Arc",
|
|
||||||
items=[{"amount": 10.20, "description": "Item costs 10.20"}],
|
|
||||||
)
|
|
||||||
invoice = await create_invoice_internal(
|
|
||||||
wallet_id=invoices_wallet.id, data=invoice_data
|
|
||||||
)
|
|
||||||
items = await create_invoice_items(invoice_id=invoice.id, data=invoice_data.items)
|
|
||||||
|
|
||||||
invoice_dict = invoice.dict()
|
|
||||||
invoice_dict["items"] = items
|
|
||||||
return invoice_dict
|
|
|
@ -1,133 +0,0 @@
|
||||||
import pytest
|
|
||||||
import pytest_asyncio # noqa: F401
|
|
||||||
from loguru import logger # noqa: F401
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet # noqa: F401
|
|
||||||
from tests.helpers import credit_wallet # noqa: F401
|
|
||||||
from tests.mocks import WALLET # noqa: F401
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_invoices_unknown_invoice(client):
|
|
||||||
response = await client.get("/invoices/pay/u")
|
|
||||||
assert response.json() == {"detail": "Invoice does not exist."}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_invoices_api_create_invoice_valid(client, invoices_wallet):
|
|
||||||
query = {
|
|
||||||
"status": "open",
|
|
||||||
"currency": "EUR",
|
|
||||||
"company_name": "LNbits, Inc.",
|
|
||||||
"first_name": "Ben",
|
|
||||||
"last_name": "Arc",
|
|
||||||
"email": "ben@legend.arc",
|
|
||||||
"items": [
|
|
||||||
{"amount": 2.34, "description": "Item 1"},
|
|
||||||
{"amount": 0.98, "description": "Item 2"},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
status = query["status"]
|
|
||||||
currency = query["currency"]
|
|
||||||
fname = query["first_name"]
|
|
||||||
total = sum(d["amount"] for d in query["items"])
|
|
||||||
|
|
||||||
response = await client.post(
|
|
||||||
"/invoices/api/v1/invoice",
|
|
||||||
json=query,
|
|
||||||
headers={"X-Api-Key": invoices_wallet.inkey},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
assert data["status"] == status
|
|
||||||
assert data["wallet"] == invoices_wallet.id
|
|
||||||
assert data["currency"] == currency
|
|
||||||
assert data["first_name"] == fname
|
|
||||||
assert sum(d["amount"] / 100 for d in data["items"]) == total
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_invoices_api_partial_pay_invoice(
|
|
||||||
client, accounting_invoice, adminkey_headers_from
|
|
||||||
):
|
|
||||||
invoice_id = accounting_invoice["id"]
|
|
||||||
amount_to_pay = int(5.05 * 100) # mock invoice total amount is 10 USD
|
|
||||||
|
|
||||||
# ask for an invoice
|
|
||||||
response = await client.post(
|
|
||||||
f"/invoices/api/v1/invoice/{invoice_id}/payments?famount={amount_to_pay}"
|
|
||||||
)
|
|
||||||
assert response.status_code < 300
|
|
||||||
data = response.json()
|
|
||||||
payment_hash = data["payment_hash"]
|
|
||||||
|
|
||||||
# pay the invoice
|
|
||||||
data = {"out": True, "bolt11": data["payment_request"]}
|
|
||||||
response = await client.post(
|
|
||||||
"/api/v1/payments", json=data, headers=adminkey_headers_from
|
|
||||||
)
|
|
||||||
assert response.status_code < 300
|
|
||||||
assert len(response.json()["payment_hash"]) == 64
|
|
||||||
assert len(response.json()["checking_id"]) > 0
|
|
||||||
|
|
||||||
# check invoice is paid
|
|
||||||
response = await client.get(
|
|
||||||
f"/invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}"
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()["paid"] is True
|
|
||||||
|
|
||||||
# check invoice status
|
|
||||||
response = await client.get(f"/invoices/api/v1/invoice/{invoice_id}")
|
|
||||||
assert response.status_code == 200
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
assert data["status"] == "open"
|
|
||||||
|
|
||||||
|
|
||||||
####
|
|
||||||
#
|
|
||||||
# TEST FAILS FOR NOW, AS LISTENERS ARE NOT WORKING ON TESTING
|
|
||||||
#
|
|
||||||
###
|
|
||||||
|
|
||||||
# @pytest.mark.asyncio
|
|
||||||
# async def test_invoices_api_full_pay_invoice(client, accounting_invoice, adminkey_headers_to):
|
|
||||||
# invoice_id = accounting_invoice["id"]
|
|
||||||
# print(accounting_invoice["id"])
|
|
||||||
# amount_to_pay = int(10.20 * 100)
|
|
||||||
|
|
||||||
# # ask for an invoice
|
|
||||||
# response = await client.post(
|
|
||||||
# f"/invoices/api/v1/invoice/{invoice_id}/payments?famount={amount_to_pay}"
|
|
||||||
# )
|
|
||||||
# assert response.status_code == 201
|
|
||||||
# data = response.json()
|
|
||||||
# payment_hash = data["payment_hash"]
|
|
||||||
|
|
||||||
# # pay the invoice
|
|
||||||
# data = {"out": True, "bolt11": data["payment_request"]}
|
|
||||||
# response = await client.post(
|
|
||||||
# "/api/v1/payments", json=data, headers=adminkey_headers_to
|
|
||||||
# )
|
|
||||||
# assert response.status_code < 300
|
|
||||||
# assert len(response.json()["payment_hash"]) == 64
|
|
||||||
# assert len(response.json()["checking_id"]) > 0
|
|
||||||
|
|
||||||
# # check invoice is paid
|
|
||||||
# response = await client.get(
|
|
||||||
# f"/invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}"
|
|
||||||
# )
|
|
||||||
# assert response.status_code == 200
|
|
||||||
# assert response.json()["paid"] is True
|
|
||||||
|
|
||||||
# # check invoice status
|
|
||||||
# response = await client.get(f"/invoices/api/v1/invoice/{invoice_id}")
|
|
||||||
# assert response.status_code == 200
|
|
||||||
# data = response.json()
|
|
||||||
|
|
||||||
# print(data)
|
|
||||||
# assert data["status"] == "paid"
|
|
Loading…
Add table
Reference in a new issue