Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions bot/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,13 +598,8 @@ const addInvoicePHI = async (
ctx.deleteMessage();
const order = await Order.findOne({ _id: orderId });
if (order === null) throw new Error('order was not found');
// orders with status PAID_HOLD_INVOICE or COMPLETED_BY_ADMIN are released payments
if (
order.status !== 'PAID_HOLD_INVOICE' &&
order.status !== 'COMPLETED_BY_ADMIN'
) {
return;
}
// only orders with status PAID_HOLD_INVOICE are released payments
if (order.status !== 'PAID_HOLD_INVOICE') return;

const buyer = await User.findOne({ _id: order.buyer_id });
if (buyer === null) return;
Expand Down
1 change: 0 additions & 1 deletion bot/modules/block/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const block = async (ctx: MainContext, username: string): Promise<void> => {
'CLOSED',
'CANCELED_BY_ADMIN',
'EXPIRED',
'COMPLETED_BY_ADMIN',
'SUCCESS',
'PAID_HOLD_INVOICE',
'CANCELED',
Expand Down
20 changes: 8 additions & 12 deletions bot/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,10 @@ const askForConfirmation = async (user: UserDocument, command: string) => {
orders = await Order.find(where);
} else if (command === '/setinvoice') {
const where: FilterQuery<OrderQuery> = {
$and: [
{ buyer_id: user._id },
{
$or: [
{ status: 'PAID_HOLD_INVOICE' },
{ status: 'COMPLETED_BY_ADMIN' },
],
},
],
buyer_id: user._id,
status: 'PAID_HOLD_INVOICE',
};

orders = await Order.find(where);
}

Expand Down Expand Up @@ -550,15 +544,17 @@ const initialize = (
}
}

if (order.secret) await settleHoldInvoice({ secret: order.secret });
if (order.secret) {
await settleHoldInvoice({ secret: order.secret });
order.settled_by_admin = true;
await order.save();
}
Comment thread
Luquitasjeffrey marked this conversation as resolved.

if (dispute) {
dispute.status = 'SETTLED';
await dispute.save();
}

order.status = 'COMPLETED_BY_ADMIN';
await order.save();
const buyer = await User.findOne({ _id: order.buyer_id });
const seller = await User.findOne({ _id: order.seller_id });
if (buyer === null || seller === null)
Expand Down
2 changes: 2 additions & 0 deletions locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ order_detail: |

Status: ${status}

Von Admin abgeschlossen: ${settledByAdmin}

Ersteller: @${creator || ''}

Käufer: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ order_detail: |

Status: ${status}

Settled by admin: ${settledByAdmin}

Creator: @${creator || ''}

Buyer: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ order_detail: |

Status: ${status}

Completada por admin: ${settledByAdmin}

Creador: @${creator || ''}

Comprador: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/fa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ order_detail: |

وضعیت: ${status}

تسویه توسط مدیر: ${settledByAdmin}

ایجاد کننده: @${creator || ''}

خریدار: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/fr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ order_detail: |

Statut : ${status}

Complété par l'administrateur : ${settledByAdmin}

Créateur : @${creator || ''}

Acheteur : @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/it.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ order_detail: |

Stato: ${status}

Completato dall'amministratore: ${settledByAdmin}

Creato da: @${creator || ''}

Acquirente: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/ko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ order_detail: |

상태: ${status}

관리자에 의해 완료됨: ${settledByAdmin}

생성자: @${creator || ''}

구매자: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/pt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ order_detail: |

Status: ${status}

Completado por admin: ${settledByAdmin}

Criadora: @${creator || ''}

Compradora: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/ru.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ order_detail: |

Статус: ${status}

Завершено администратором: ${settledByAdmin}

Создатель: @${creator || ''}

Покупатель: @${buyerUsername || ''}
Expand Down
2 changes: 2 additions & 0 deletions locales/uk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ order_detail: |

Статус: ${status}

Завершено адміністратором: ${settledByAdmin}

Автор: @${creator || ''}

Покупець: @${buyerUsername || ''}
Expand Down
3 changes: 2 additions & 1 deletion models/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface IOrder extends Document {
is_public: boolean;
random_image: string;
is_golden_honey_badger?: boolean;
settled_by_admin?: boolean;
}

const orderSchema = new Schema<IOrder, mongoose.Model<IOrder>>({
Expand Down Expand Up @@ -118,10 +119,10 @@ const orderSchema = new Schema<IOrder, mongoose.Model<IOrder>>({
'PAID_HOLD_INVOICE', // seller released funds
'CANCELED_BY_ADMIN',
'EXPIRED', // Expired orders, stated changed by a job
'COMPLETED_BY_ADMIN',
'FROZEN',
],
},
settled_by_admin: { type: Boolean, default: false },
type: { type: String },
fiat_amount: { type: Number, min: 1 }, // amount in fiat
fiat_code: { type: String },
Expand Down
39 changes: 39 additions & 0 deletions scripts/migrate_completed_by_admin_orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'dotenv/config';
import { connect as mongoConnect } from '../db_connect';
import Order from '../models/order';
import { logger } from '../logger';

const migrate = async () => {
try {
const mongoose = mongoConnect();
await new Promise((resolve, reject) => {
mongoose.connection.once('open', resolve);
mongoose.connection.on('error', reject);
});

logger.info('Connected to MongoDB for migration.');

const query = { status: 'COMPLETED_BY_ADMIN' };
const update = {
$set: {
status: 'SUCCESS',
settled_by_admin: true,
},
};

const result = await Order.updateMany(query, update);

logger.info(`Migration completed.`);
logger.info(`Matched: ${result.matchedCount} orders.`);
logger.info(`Modified: ${result.modifiedCount} orders.`);

await mongoose.connection.close();
logger.info('Database connection closed.');
process.exit(0);
} catch (error) {
logger.error(`Migration failed: ${error}`);
process.exit(1);
}
};

migrate();
106 changes: 106 additions & 0 deletions tests/bot/bot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,4 +609,110 @@ describe('Bot Initialization', () => {
ctx.reply.calledWithExactly('This is an unknown command.'),
).to.be.equal(true);
});

it('should set settled_by_admin when admin settles with secret', async () => {
const orderMock = {
_id: 'orderId',
status: 'DISPUTE',
secret: 'secret',
community_id: null,
buyer_id: 'buyer',
seller_id: 'seller',
save: sinon.stub().resolves(),
} as any;

const OrderFindOneStub = sinon.stub().resolves(orderMock);
const settleHoldInvoiceStub = sinon.stub().resolves();

const startModule = proxyquire('../../bot/start', {
telegraf: { Telegraf: sinon.stub().returns(botStub) },
'../models': {
Order: { findOne: OrderFindOneStub },
User: { findOne: sinon.stub().resolves({ id: 'user' }) },
Dispute: { findOne: sinon.stub().resolves(null) },
},
'./validations': {
validateParams: sinon.stub().resolves(['orderId']),
validateObjectId: sinon.stub().resolves(true),
},
'../ln': { settleHoldInvoice: settleHoldInvoiceStub },
'./messages': {
successCompleteOrderMessage: sinon.stub().resolves(),
successCompleteOrderByAdminMessage: sinon.stub().resolves(),
},
});

startModule.initialize('dummy-token', {});
const settleOrderCall = botStub.command
.getCalls()
.find((c: any) => c.args[0] === 'settleorder');
const handler = settleOrderCall.args[2];

const ctx = {
admin: { admin: true },
match: ['/settleorder orderId', 'orderId'],
reply: sinon.stub().resolves(),
i18n: { t: sinon.stub().returns('Success') },
telegram: { sendMessage: sinon.stub().resolves() },
};

await handler(ctx);

expect(settleHoldInvoiceStub.calledWith({ secret: 'secret' })).to.be.equal(
true,
);
expect(orderMock.settled_by_admin).to.be.equal(true);
expect(orderMock.save.called).to.be.equal(true);
});

it('should not modify order if settleHoldInvoice fails', async () => {
const orderMock = {
_id: 'orderId',
status: 'DISPUTE',
secret: 'invalidsecret',
community_id: null,
buyer_id: 'buyer',
seller_id: 'seller',
save: sinon.stub().resolves(),
} as any;

const OrderFindOneStub = sinon.stub().resolves(orderMock);
const settleHoldInvoiceStub = sinon.stub().rejects(new Error('LND failed'));

const startModule = proxyquire('../../bot/start', {
telegraf: { Telegraf: sinon.stub().returns(botStub) },
'../models': {
Order: { findOne: OrderFindOneStub },
User: { findOne: sinon.stub().resolves({ id: 'user' }) },
Dispute: { findOne: sinon.stub().resolves(null) },
},
'./validations': {
validateParams: sinon.stub().resolves(['orderId']),
validateObjectId: sinon.stub().resolves(true),
},
'../ln': { settleHoldInvoice: settleHoldInvoiceStub },
'./messages': {
successCompleteOrderMessage: sinon.stub().resolves(),
successCompleteOrderByAdminMessage: sinon.stub().resolves(),
},
});

startModule.initialize('dummy-token', {});
const settleOrderCall = botStub.command
.getCalls()
.find((c: any) => c.args[0] === 'settleorder');
const handler = settleOrderCall.args[2];

const ctx = {
admin: { admin: true },
match: ['/settleorder orderId', 'orderId'],
reply: sinon.stub().resolves(),
i18n: { t: sinon.stub().returns('Success') },
};

await handler(ctx);

expect(orderMock.save.called).to.be.equal(false);
expect(orderMock.settled_by_admin).to.equal(undefined);
});
Comment on lines +668 to +717
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Test stub behavior doesn't match production settleHoldInvoice implementation.

Per the relevant code snippet from ln/hold_invoice.ts:43-49, the real settleHoldInvoice catches errors internally and logs them—it never rejects. This test stubs it to reject, which verifies handler behavior for a scenario that can't occur in production.

In practice, if LN settlement fails:

  1. settleHoldInvoice catches the error, logs it, and returns normally
  2. Handler continues to set settled_by_admin = true and calls save()
  3. Order is incorrectly marked as settled by admin (false positive)

Consider either:

  • Adding a test that stubs settleHoldInvoice to resolve (mimicking silent failure) and verifying the false-positive scenario, or
  • If the intent is to refactor settleHoldInvoice to propagate errors, add a TODO comment noting this dependency
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/bot/bot.spec.ts` around lines 668 - 717, The test currently stubs
settleHoldInvoice to reject, which doesn't match the real function
(ln/hold_invoice.ts::settleHoldInvoice) that catches errors and does not reject;
update the test in bot.spec.ts to stub ../ln.settleHoldInvoice to resolve (e.g.,
resolves()), then adjust assertions to expect orderMock.save to have been called
and orderMock.settled_by_admin to be true to reproduce the real-world "silent
failure" false-positive; alternatively, if you intend to change production
behavior to propagate errors, add a TODO in ln/hold_invoice.ts near
settleHoldInvoice noting tests rely on its swallowing errors and add a new test
after refactor to assert rejection behavior.

});
Loading
Loading