diff --git a/api/admin.py b/api/admin.py
index 01d4f2d5..0ca8e536 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -210,6 +210,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
f"Dispute of order {order.id} solved successfully on favor of the maker",
messages.SUCCESS,
)
+ send_notification.delay(order_id=order.id, message="dispute_closed")
else:
self.message_user(
@@ -248,6 +249,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
f"Dispute of order {order.id} solved successfully on favor of the taker",
messages.SUCCESS,
)
+ send_notification.delay(order_id=order.id, message="dispute_closed")
else:
self.message_user(
diff --git a/api/migrations/0048_alter_order_reference.py b/api/migrations/0048_alter_order_reference.py
new file mode 100644
index 00000000..cf8fa076
--- /dev/null
+++ b/api/migrations/0048_alter_order_reference.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.6 on 2024-06-29 14:07
+
+import api.models.order
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0047_notification'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='order',
+ name='reference',
+ field=models.UUIDField(default=api.models.order.custom_uuid, editable=False),
+ ),
+ ]
diff --git a/api/models/order.py b/api/models/order.py
index 161f6d3e..f9a72ecf 100644
--- a/api/models/order.py
+++ b/api/models/order.py
@@ -10,6 +10,7 @@ from django.db import models
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.utils import timezone
+from api.tasks import send_notification
if config("TESTING", cast=bool, default=False):
import random
@@ -91,10 +92,7 @@ class Order(models.Model):
decimal_places=2,
default=0,
null=True,
- validators=[
- MinValueValidator(Decimal(-100)),
- MaxValueValidator(Decimal(999))
- ],
+ validators=[MinValueValidator(Decimal(-100)), MaxValueValidator(Decimal(999))],
blank=True,
)
# explicit
@@ -352,6 +350,8 @@ class Order(models.Model):
self.log(
f"Order state went from {old_status}: {Order.Status(old_status).label} to {new_status}: {Order.Status(new_status).label}"
)
+ if new_status == Order.Status.FAI:
+ send_notification.delay(order_id=self.id, message="lightning_failed")
@receiver(pre_delete, sender=Order)
diff --git a/api/models/robot.py b/api/models/robot.py
index 9efed1c1..c838d354 100644
--- a/api/models/robot.py
+++ b/api/models/robot.py
@@ -1,12 +1,8 @@
-from pathlib import Path
-
-from django.conf import settings
from django.contrib.auth.models import User
from django.core.validators import validate_comma_separated_integer_list
from django.db import models
-from django.db.models.signals import post_save, pre_delete
+from django.db.models.signals import post_save
from django.dispatch import receiver
-from django.utils.html import mark_safe
class Robot(models.Model):
diff --git a/api/notifications.py b/api/notifications.py
index 480c6d47..7adc9f60 100644
--- a/api/notifications.py
+++ b/api/notifications.py
@@ -219,3 +219,47 @@ class Notifications:
title = f"๐ ๏ธ Your order with ID {order.id} has been cancelled by the coordinator {config('COORDINATOR_ALIAS', cast=str, default='NoAlias')} for the upcoming maintenance stop."
self.send_message(order, order.maker.robot, title)
return
+
+ def dispute_closed(self, order):
+ lang = order.maker.robot.telegram_lang_code
+ if order.status == Order.Status.MLD:
+ # Maker lost dispute
+ looser = order.maker
+ winner = order.taker
+ elif order.status == Order.Status.TLD:
+ # Taker lost dispute
+ looser = order.taker
+ winner = order.maker
+
+ lang = looser.robot.telegram_lang_code
+ if lang == "es":
+ title = f"โ๏ธ Hey {looser.username}, has perdido la disputa en la orden con ID {str(order.id)}."
+ else:
+ title = f"โ๏ธ Hey {looser.username}, you lost the dispute on your order with ID {str(order.id)}."
+ self.send_message(order, looser.robot, title)
+
+ lang = winner.robot.telegram_lang_code
+ if lang == "es":
+ title = f"โ๏ธ Hey {winner.username}, has ganado la disputa en la orden con ID {str(order.id)}."
+ else:
+ title = f"โ๏ธ Hey {winner.username}, you won the dispute on your order with ID {str(order.id)}."
+ self.send_message(order, winner.robot, title)
+
+ return
+
+ def lightning_failed(self, order):
+ lang = order.maker.robot.telegram_lang_code
+ if order.type == Order.Types.BUY:
+ buyer = order.maker
+ else:
+ buyer = order.taker
+
+ if lang == "es":
+ title = f"โกโ Hey {buyer.username}, el pago lightning en la order con ID {str(order.id)} ha fallado."
+ description = "Intentalo de nuevo con una nueva factura o con otra wallet."
+ else:
+ title = f"โกโ Hey {buyer.username}, the lightning payment on your order with ID {str(order.id)} failed."
+ description = "Try again with a new invoice or from another wallet."
+
+ self.send_message(order, buyer.robot, title, description)
+ return
diff --git a/api/serializers.py b/api/serializers.py
index 2f7b2310..46fb1c58 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -491,9 +491,16 @@ class OrderDetailSerializer(serializers.ModelSerializer):
class ListNotificationSerializer(serializers.ModelSerializer):
+ status = serializers.SerializerMethodField(
+ help_text="The `status` of the order when the notification was trigered",
+ )
+
class Meta:
model = Notification
- fields = ("title", "description", "order_id")
+ fields = ("title", "description", "order_id", "status", "created_at")
+
+ def get_status(self, notification) -> int:
+ return notification.order.status
class OrderPublicSerializer(serializers.ModelSerializer):
diff --git a/api/tasks.py b/api/tasks.py
index 88e924a8..656feb89 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -303,4 +303,10 @@ def send_notification(order_id=None, chat_message_id=None, message=None):
elif message == "coordinator_cancelled":
notifications.coordinator_cancelled(order)
+ elif message == "dispute_closed":
+ notifications.dispute_closed(order)
+
+ elif message == "lightning_failed":
+ notifications.lightning_failed(order)
+
return
diff --git a/api/views.py b/api/views.py
index 0caa7407..a03dbd33 100644
--- a/api/views.py
+++ b/api/views.py
@@ -761,10 +761,7 @@ class NotificationsView(ListAPIView):
notification_data = []
for notification in queryset:
data = self.serializer_class(notification).data
- data["title"] = str(notification.title)
- data["description"] = str(notification.description)
data["order_id"] = notification.order.id
-
notification_data.append(data)
return Response(notification_data, status=status.HTTP_200_OK)
diff --git a/docker-tests.yml b/docker-tests.yml
index 0a4af45e..975747e8 100644
--- a/docker-tests.yml
+++ b/docker-tests.yml
@@ -15,7 +15,7 @@ version: '3.9'
services:
bitcoind:
image: ruimarinho/bitcoin-core:${BITCOIND_VERSION:-24.0.1}-alpine
- container_name: btc
+ container_name: test-btc
restart: always
ports:
- "8000:8000"
@@ -50,7 +50,7 @@ services:
coordinator-LND:
image: lightninglabs/lnd:${LND_VERSION:-v0.17.0-beta}
- container_name: coordinator-LND
+ container_name: test-coordinator-LND
restart: always
volumes:
- bitcoin:/root/.bitcoin/
@@ -83,7 +83,7 @@ services:
coordinator-CLN:
image: elementsproject/lightningd:${CLN_VERSION:-v24.05}
restart: always
- container_name: coordinator-CLN
+ container_name: test-coordinator-CLN
environment:
LIGHTNINGD_NETWORK: 'regtest'
volumes:
@@ -97,7 +97,7 @@ services:
robot-LND:
image: lightninglabs/lnd:${LND_VERSION:-v0.17.0-beta}
- container_name: robot-LND
+ container_name: test-robot-LND
restart: always
volumes:
- bitcoin:/root/.bitcoin/
@@ -129,7 +129,7 @@ services:
redis:
image: redis:${REDIS_VERSION:-7.2.1}-alpine
- container_name: redis
+ container_name: test-redis
restart: always
volumes:
- redisdata:/data
@@ -141,7 +141,7 @@ services:
args:
DEVELOPMENT: True
image: backend-image
- container_name: coordinator
+ container_name: test-coordinator
restart: always
environment:
DEVELOPMENT: True
@@ -171,7 +171,7 @@ services:
postgres:
image: postgres:${POSTGRES_VERSION:-14.2}-alpine
- container_name: sql
+ container_name: test-sql
restart: always
environment:
POSTGRES_PASSWORD: 'example'
diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml
index 60c5c933..490c35ae 100644
--- a/docs/assets/schemas/api-latest.yaml
+++ b/docs/assets/schemas/api-latest.yaml
@@ -1106,8 +1106,16 @@ components:
order_id:
type: integer
readOnly: true
+ status:
+ type: integer
+ readOnly: true
+ description: The `status` of the order when the notification was trigered
+ created_at:
+ type: string
+ format: date-time
required:
- order_id
+ - status
ListOrder:
type: object
properties:
diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py
index bf438a48..c226b5c0 100644
--- a/tests/test_trade_pipeline.py
+++ b/tests/test_trade_pipeline.py
@@ -1163,6 +1163,60 @@ class TradeTest(BaseAPITestCase):
f"โ๏ธ Hey {data['taker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
)
+ # def test_dispute_closed_maker_wins(self):
+ # trade = Trade(self.client)
+ # trade.publish_order()
+ # trade.take_order()
+ # trade.lock_taker_bond()
+ # trade.lock_escrow(trade.taker_index)
+ # trade.submit_payout_invoice(trade.maker_index)
+
+ # # Admin resolves dispute
+
+ # trade.clean_orders()
+
+ # maker_headers = trade.get_robot_auth(trade.maker_index)
+ # response = self.client.get(reverse("notifications"), **maker_headers)
+ # self.assertResponse(response)
+ # notifications_data = list(response.json())
+ # self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
+ # self.assertEqual(
+ # notifications_data[0]["title"],
+ # f"โ๏ธ Hey {data['maker_nick']}, you won the dispute on your order with ID {str(trade.order_id)}."
+ # )
+ # taker_headers = trade.get_robot_auth(trade.taker_index)
+ # response = self.client.get(reverse("notifications"), **taker_headers)
+ # self.assertResponse(response)
+ # notifications_data = list(response.json())
+ # self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
+ # self.assertEqual(
+ # notifications_data[0]["title"],
+ # f"โ๏ธ Hey {data['taker_nick']}, you lost the dispute on your order with ID {str(trade.order_id)}."
+ # )
+
+ def test_lightning_payment_failed(self):
+ trade = Trade(self.client)
+ trade.publish_order()
+ trade.take_order()
+ trade.lock_taker_bond()
+ trade.lock_escrow(trade.taker_index)
+ trade.submit_payout_invoice(trade.maker_index)
+
+ trade.change_order_status(Order.Status.FAI)
+
+ trade.clean_orders()
+
+ maker_headers = trade.get_robot_auth(trade.maker_index)
+ maker_nick = read_file(f"tests/robots/{trade.maker_index}/nickname")
+ response = self.client.get(reverse("notifications"), **maker_headers)
+ self.assertResponse(response)
+ notifications_data = list(response.json())
+ self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
+ self.assertEqual(
+ notifications_data[0]["title"],
+ f"โกโ Hey {maker_nick}, the lightning payment on your order with ID {str(trade.order_id)} failed.",
+ )
+
def test_withdraw_reward_after_unilateral_cancel(self):
"""
Tests withdraw rewards as taker after maker cancels order unilaterally
diff --git a/tests/utils/trade.py b/tests/utils/trade.py
index c00b8757..39bcda0a 100644
--- a/tests/utils/trade.py
+++ b/tests/utils/trade.py
@@ -271,3 +271,9 @@ class Trade:
order = Order.objects.get(id=self.order_id)
order.expires_at = datetime.now()
order.save()
+
+ @patch("api.tasks.send_notification.delay", send_notification)
+ def change_order_status(self, status):
+ # Change order expiry to now
+ order = Order.objects.get(id=self.order_id)
+ order.update_status(status)